Repository: goodhertz/coldtype Branch: main Commit: f1f911cf8c5b Files: 507 Total size: 5.4 MB Directory structure: gitextract_ga8pht78/ ├── .coldtype.win.py ├── .gitignore ├── .python-version ├── CHANGELOG.md ├── LICENSE ├── README_EXTRAS.md ├── assets/ │ ├── .gitignore │ ├── ColdtypeObviously.designspace │ ├── ColdtypeObviously_BlackItalic.ufo/ │ │ ├── fontinfo.plist │ │ ├── glyphs/ │ │ │ ├── C_.glif │ │ │ ├── D_.glif │ │ │ ├── E_.glif │ │ │ ├── L_.glif │ │ │ ├── O_.glif │ │ │ ├── P_.glif │ │ │ ├── T_.glif │ │ │ ├── Y_.glif │ │ │ ├── contents.plist │ │ │ ├── layerinfo.plist │ │ │ └── space.glif │ │ ├── groups.plist │ │ ├── layercontents.plist │ │ ├── lib.plist │ │ └── metainfo.plist │ ├── ColdtypeObviously_CompressedBlackItalic.otf │ ├── ColdtypeObviously_CompressedBlackItalic.ufo/ │ │ ├── fontinfo.plist │ │ ├── glyphs/ │ │ │ ├── C_.glif │ │ │ ├── D_.glif │ │ │ ├── E_.glif │ │ │ ├── L_.glif │ │ │ ├── O_.glif │ │ │ ├── P_.glif │ │ │ ├── T_.glif │ │ │ ├── Y_.glif │ │ │ ├── contents.plist │ │ │ ├── layerinfo.plist │ │ │ └── space.glif │ │ ├── groups.plist │ │ ├── layercontents.plist │ │ ├── lib.plist │ │ └── metainfo.plist │ ├── README.md │ ├── logos.ufo/ │ │ ├── fontinfo.plist │ │ ├── glyphs/ │ │ │ ├── contents.plist │ │ │ ├── goodhertz_logo_2019.glif │ │ │ └── layerinfo.plist │ │ ├── glyphs.background/ │ │ │ ├── contents.plist │ │ │ └── layerinfo.plist │ │ ├── layercontents.plist │ │ ├── lib.plist │ │ └── metainfo.plist │ └── noto.py ├── buildenv ├── docs/ │ ├── .gitignore │ └── tutorials/ │ ├── midi.rst │ └── type_design.rst ├── examples/ │ ├── .gitignore │ ├── alphabet.py │ ├── animations/ │ │ ├── 808.py │ │ ├── _audio.py │ │ ├── _drumsolo.py │ │ ├── _simple.py │ │ ├── access_frame.py │ │ ├── adsr.py │ │ ├── adsr_ascii.py │ │ ├── alphabet.py │ │ ├── alternate_glyphs.py │ │ ├── ascii_choreography.py │ │ ├── ascii_keyframe_positions.py │ │ ├── ascii_keyframes.py │ │ ├── ascii_keyframes2.py │ │ ├── ascii_keyframes_entrance.py │ │ ├── ascii_pixels.py │ │ ├── ascii_simple.py │ │ ├── ascii_twostep.py │ │ ├── ascii_words.py │ │ ├── avoidance.py │ │ ├── banner.py │ │ ├── bitmap_font.py │ │ ├── blendmode.py │ │ ├── bounce.py │ │ ├── colrv1_foldit.py │ │ ├── colrv1_nabla.py │ │ ├── countdown.py │ │ ├── custom_ease.py │ │ ├── custom_output.py │ │ ├── delay.py │ │ ├── drumsolo2.py │ │ ├── dswatch.py │ │ ├── dvd.py │ │ ├── ec.py │ │ ├── flyin.py │ │ ├── glyphwise.py │ │ ├── glyphwise2_rtl.py │ │ ├── glyphwise_keyframes.py │ │ ├── glyphwise_wave.py │ │ ├── glyphwise_wave2.py │ │ ├── house.py │ │ ├── interpolate_roughen.py │ │ ├── ipa_vowels.py │ │ ├── ives.py │ │ ├── letters_easing.py │ │ ├── linewise.py │ │ ├── midi_cc.py │ │ ├── moire1.py │ │ ├── officehours.py │ │ ├── original_demo.py │ │ ├── penangle.py │ │ ├── physics2d.py │ │ ├── pixels.py │ │ ├── pseudomorph.py │ │ ├── recursive_shape.py │ │ ├── recursive_text.py │ │ ├── retails/ │ │ │ ├── casual.py │ │ │ ├── chopper.py │ │ │ ├── colorfont.py │ │ │ ├── digestive_snake.py │ │ │ ├── digestive_wind.py │ │ │ ├── gridsystems.py │ │ │ ├── hansjorg.py │ │ │ ├── montreuil.py │ │ │ ├── stacked_and_justified.py │ │ │ ├── vulfbach.py │ │ │ ├── wavinghand.py │ │ │ └── welcome.py │ │ ├── rgbsplit.py │ │ ├── roundandround.py │ │ ├── separation.py │ │ ├── simple_recording.json │ │ ├── simplevarfont.py │ │ ├── slicer.py │ │ ├── sonification.py │ │ ├── spreadstack.py │ │ ├── superoutline.py │ │ ├── tapered_shadow.py │ │ ├── texttopoints.py │ │ ├── transparent_gifski.py │ │ ├── transparent_understroke.py │ │ ├── truchet.py │ │ ├── truchet3.py │ │ ├── twister.py │ │ ├── ulrich_e.py │ │ ├── versioned.py │ │ ├── versioned_with_sidecar.py │ │ ├── versioned_with_sidecar_versions.py │ │ ├── vertical_scale.py │ │ ├── warpblur.py │ │ └── wheee.py │ ├── apkjr.py │ ├── apng.py │ ├── axidraw/ │ │ ├── hatching.py │ │ ├── nextdraw.py │ │ ├── sheet.py │ │ └── sheet_read.py │ ├── bg_fn.py │ ├── bg_img.py │ ├── blender/ │ │ ├── arch.py │ │ ├── array_separate.py │ │ ├── bauhaus_book_14.py │ │ ├── boston.py │ │ ├── direct_objects.py │ │ ├── displace.py │ │ ├── dof.py │ │ ├── dominos.py │ │ ├── dominos2.py │ │ ├── dominos3.py │ │ ├── hobeauxborders.py │ │ ├── ifg.py │ │ ├── img.py │ │ ├── liveimage.py │ │ ├── noordzijcube.py │ │ ├── parched.py │ │ ├── physics_direct.py │ │ ├── physics_upright.py │ │ ├── reprojection.py │ │ ├── rome.py │ │ ├── rome_preview.py │ │ ├── rotating.py │ │ ├── sequence.py │ │ ├── sequence_text3d.py │ │ ├── sequence_text3d_rich.py │ │ ├── simple_single.py │ │ ├── simplebeat.py │ │ ├── timedtext.py │ │ ├── varfont.py │ │ ├── varfont2.py │ │ └── wip/ │ │ ├── bake.py │ │ ├── blends/ │ │ │ └── boston.blend.json │ │ ├── boston.py │ │ ├── bump.py │ │ ├── physics.py │ │ ├── physics_semi2d.py │ │ ├── physics_visible.py │ │ └── timed3d.py │ ├── blog.py │ ├── borders.py │ ├── chessboard.py │ ├── circle_text.py │ ├── colrv1_arabic.py │ ├── cropandrepeat.py │ ├── custom_hotkey.py │ ├── diagram.py │ ├── direct_uharfbuzz.py │ ├── drawbot/ │ │ ├── both.py │ │ ├── composition.py │ │ ├── pdfdoc.py │ │ ├── pixellation.py │ │ └── varfont.py │ ├── easing.py │ ├── example.py │ ├── freeze.py │ ├── github_social.py │ ├── grid_shapes.py │ ├── image_in_path.py │ ├── image_rotate.py │ ├── image_rotated_quality.py │ ├── instancer.py │ ├── interpolated_spiral.py │ ├── interrupted_lines.py │ ├── layers.py │ ├── letter_lighttrail.py │ ├── linealigning.py │ ├── linebreaking.py │ ├── logo.py │ ├── logo_state.json │ ├── metaprogramming.py │ ├── mirror.py │ ├── misc/ │ │ └── no_command_line.py │ ├── opentypesvgimagefont.py │ ├── potracer.py │ ├── printer.py │ ├── random_shape.py │ ├── restmake.py │ ├── richtext.py │ ├── rounded_corners.py │ ├── scaffold.py │ ├── scripts/ │ │ ├── player.py │ │ ├── prores.py │ │ ├── prores_to_frames.py │ │ └── symbolfinder.py │ ├── shapes.py │ ├── simple.py │ ├── simplest.py │ ├── sites/ │ │ ├── .gitignore │ │ ├── blog.coldtype.xyz/ │ │ │ ├── .gitignore │ │ │ ├── assets/ │ │ │ │ └── style.css │ │ │ ├── blog.coldtype.xyz.py │ │ │ ├── pages/ │ │ │ │ └── posts/ │ │ │ │ ├── a-blog.ipynb │ │ │ │ ├── transparent-unclickable.ipynb │ │ │ │ └── truchet-experiments.ipynb │ │ │ └── templates/ │ │ │ ├── _footer.j2 │ │ │ ├── _header.j2 │ │ │ ├── _post.j2 │ │ │ └── index.j2 │ │ ├── coldtype.goodhertz.com/ │ │ │ ├── assets/ │ │ │ │ └── style.css │ │ │ ├── coldtype.goodhertz.com.py │ │ │ ├── pages/ │ │ │ │ ├── about.ipynb │ │ │ │ ├── cheatsheets/ │ │ │ │ │ ├── easing.md │ │ │ │ │ ├── oneletter.md │ │ │ │ │ ├── rectangles.ipynb │ │ │ │ │ ├── text.ipynb │ │ │ │ │ └── viewer.md │ │ │ │ ├── classes_functions.ipynb │ │ │ │ ├── install.ipynb │ │ │ │ ├── introduction.ipynb │ │ │ │ ├── overview.ipynb │ │ │ │ └── tutorials/ │ │ │ │ ├── animation.ipynb │ │ │ │ ├── blender.ipynb │ │ │ │ ├── drawbot.ipynb │ │ │ │ ├── geometry.ipynb │ │ │ │ ├── shapes.ipynb │ │ │ │ └── text.ipynb │ │ │ └── templates/ │ │ │ ├── _docs.j2 │ │ │ ├── _footer.j2 │ │ │ ├── _header.j2 │ │ │ ├── _page.j2 │ │ │ ├── index.j2 │ │ │ └── partials/ │ │ │ └── sidebar.j2 │ │ ├── coldtype.p5js/ │ │ │ ├── assets/ │ │ │ │ ├── hb.wasm │ │ │ │ ├── hbjs.js │ │ │ │ └── script.js │ │ │ └── coldtype.p5js.py │ │ ├── coldtype.xyz/ │ │ │ └── coldtype.xyz.py │ │ ├── portfolio/ │ │ │ ├── assets/ │ │ │ │ └── style.css │ │ │ ├── build.py │ │ │ ├── pages/ │ │ │ │ ├── about.md │ │ │ │ └── posts/ │ │ │ │ └── example.md │ │ │ └── templates/ │ │ │ ├── _footer.j2 │ │ │ ├── _header.j2 │ │ │ ├── _page.j2 │ │ │ ├── _post.j2 │ │ │ └── index.j2 │ │ └── skeleton/ │ │ └── skeleton.py │ ├── skia_direct.py │ ├── skia_paragraph.py │ ├── skia_shader.py │ ├── skia_shader.sksl │ ├── skia_shader2.py │ ├── skia_shader_clouds.sksl │ ├── snakes.py │ ├── spacing_clusters.py │ ├── src_macro.py │ ├── stacking.py │ ├── svg_viewer.py │ ├── transparency.py │ ├── transparent_understroke.py │ ├── ufo.py │ ├── vector_pixels.py │ └── wip/ │ ├── capture.py │ ├── displace_map.py │ ├── drawbot_image.py │ ├── google_font.py │ ├── toggle.py │ └── ui.py ├── packages/ │ ├── coldtype/ │ │ ├── README.md │ │ └── pyproject.toml │ └── coldtype-core/ │ ├── MANIFEST.in │ ├── README.md │ ├── pyproject.toml │ └── src/ │ └── coldtype/ │ ├── __init__.py │ ├── __main__.py │ ├── assets/ │ │ └── glyphNamesToUnicode.txt │ ├── axidraw.py │ ├── beziers.py │ ├── blender/ │ │ ├── __init__.py │ │ ├── fluent.py │ │ ├── livepreview.py │ │ ├── panel3d.py │ │ ├── render.py │ │ ├── timedtext.py │ │ ├── util.py │ │ └── watch.py │ ├── capture/ │ │ └── __init__.py │ ├── color/ │ │ ├── __init__.py │ │ └── html.py │ ├── css.py │ ├── demo/ │ │ ├── blank.py │ │ ├── boiler.py │ │ ├── boiler_renderable.py │ │ ├── demo.py │ │ ├── demoblender.py │ │ ├── docstrings.py │ │ ├── gifski.py │ │ └── glfw34.py │ ├── drawbot.py │ ├── fx/ │ │ ├── chainable.py │ │ ├── diagram.py │ │ ├── motion.py │ │ ├── shapes.py │ │ ├── skia.py │ │ ├── warping.py │ │ └── xray.py │ ├── geometry/ │ │ ├── __init__.py │ │ ├── atom.py │ │ ├── curve.py │ │ ├── edge.py │ │ ├── geometrical.py │ │ ├── line.py │ │ ├── point.py │ │ ├── primitives.py │ │ └── rect.py │ ├── grid/ │ │ └── __init__.py │ ├── helpers.py │ ├── img/ │ │ ├── abstract.py │ │ ├── blendmode.py │ │ ├── drawbotimage.py │ │ ├── skiaimage.py │ │ └── skiasvg.py │ ├── interpolation/ │ │ └── __init__.py │ ├── midi/ │ │ └── controllers.py │ ├── notebook/ │ │ ├── __init__.py │ │ └── parser.py │ ├── osutil.py │ ├── pens/ │ │ ├── axidrawpen.py │ │ ├── blenderpen.py │ │ ├── drawablepen.py │ │ ├── drawbotpen.py │ │ ├── jsonpen.py │ │ ├── misc.py │ │ ├── outlinepen.py │ │ ├── rendererdrawbotpen.py │ │ ├── reportlabpen.py │ │ ├── skiapathpen.py │ │ ├── skiapen.py │ │ ├── svgpen.py │ │ └── translationpen.py │ ├── physics/ │ │ └── pymunk.py │ ├── random.py │ ├── raster.py │ ├── renderable/ │ │ ├── __init__.py │ │ ├── animation.py │ │ ├── renderable.py │ │ ├── tools.py │ │ └── ui.py │ ├── renderer/ │ │ ├── __init__.py │ │ ├── config.py │ │ ├── keyboard.py │ │ ├── reader.py │ │ ├── state.py │ │ ├── ui.py │ │ ├── utils.py │ │ └── winman/ │ │ ├── __init__.py │ │ ├── audio.py │ │ ├── blender.py │ │ ├── glfwskia.py │ │ ├── midi.py │ │ └── passthrough.py │ ├── runon/ │ │ ├── __init__.py │ │ ├── _path.py │ │ ├── mixins/ │ │ │ ├── DrawingMixin.py │ │ │ ├── FXMixin.py │ │ │ ├── GeometryMixin.py │ │ │ ├── GlyphMixin.py │ │ │ ├── LayoutMixin.py │ │ │ ├── PathopsMixin.py │ │ │ ├── SegmentingMixin.py │ │ │ ├── SerializationMixin.py │ │ │ ├── SonificationMixin.py │ │ │ └── StylingMixin.py │ │ ├── path.py │ │ ├── runon.py │ │ └── scaffold.py │ ├── skiashim.py │ ├── test.py │ ├── text/ │ │ ├── __init__.py │ │ ├── colr/ │ │ │ ├── brsurface.py │ │ │ └── skia.py │ │ ├── composer.py │ │ ├── font.py │ │ ├── reader.py │ │ ├── richtext.py │ │ └── shaper.py │ ├── timing/ │ │ ├── __init__.py │ │ ├── audio.py │ │ ├── clip.py │ │ ├── easing.py │ │ ├── midi.py │ │ ├── nle/ │ │ │ ├── .gitignore │ │ │ ├── ableton.py │ │ │ ├── ascii.py │ │ │ └── premiere.py │ │ ├── sequence.py │ │ ├── timeable.py │ │ ├── timeline.py │ │ └── viewer.py │ ├── tool.py │ ├── tools/ │ │ ├── chars.py │ │ ├── dsview.py │ │ ├── find.py │ │ ├── findappicon.py │ │ ├── glyphloop.py │ │ ├── glyphs.py │ │ ├── instances.py │ │ ├── midi.py │ │ ├── midicc.py │ │ ├── vf.py │ │ └── viewseq.py │ ├── warping.py │ └── web/ │ ├── fonts.py │ ├── page.py │ ├── server.py │ ├── site.py │ └── templates/ │ ├── notebook.j2 │ └── page.j2 ├── pyproject.toml ├── release.sh ├── run_tests.sh ├── scripts/ │ ├── inline_mixins.py │ ├── keyboard_layout_converter.py │ └── robofont_coldtype.py ├── test/ │ ├── drawbot/ │ │ ├── db_cli.py │ │ ├── direct_import.py │ │ └── style_test.py │ ├── source_file.py │ ├── source_file_adjacent.py │ ├── source_file_with_config.py │ ├── test_geometry.py │ ├── test_helpers.py │ ├── test_p.py │ ├── test_pens.py │ ├── test_pens_rendered.py │ ├── test_runon.py │ ├── test_syntax_mods.py │ ├── test_time.py │ └── visuals/ │ ├── .gitignore │ ├── test_color_palette.py │ ├── test_gs.py │ ├── test_image_font.py │ ├── test_midi_ctrl.py │ └── test_reader_mod.py ├── tests/ │ ├── _img_only.py │ ├── test_color.py │ ├── test_drawbot.py │ ├── test_fonts.py │ ├── test_fx.py │ ├── test_glyphwise.py │ ├── test_i18n.py │ ├── test_pens.py │ ├── test_reader.py │ ├── test_rect.py │ ├── test_richtext.py │ ├── test_scaffold.py │ ├── test_src_macro.py │ ├── test_text.py │ ├── test_versions.py │ └── test_versions_versions.py └── upload_docs.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coldtype.win.py ================================================ WINDOW_PIN = "E" WINDOW_CONTENT_SCALE = 1 WINDOW_FLOAT = 1 #WINDOW_TRANSPARENT = 1 # this should be a properly-configured benv using b3denv # BLENDER_PATH = "C:/Program Files/Blender Foundation/Blender 3.3/blender.exe" from pathlib import Path FFMPEG_COMMAND = Path("~/Downloads/ffmpeg-master-latest-win64-gpl/ffmpeg-master-latest-win64-gpl/bin/ffmpeg.exe").expanduser() ================================================ FILE: .gitignore ================================================ .DS_Store .vscode .idea env venv venv2 venv37 venv-arm venv_x64 venv* benv benv* test/visuals/artifacts/* test/visuals/*.blend1 test/visuals/test_media test/visuals/pdfs build dist coldtype.egg-info packages/**/*.egg-info __pycache__ *.pyc *.patch coldtype/scratch.svg *_frames *_layers renders media scratch scratch.py scratch* .coldtype.py recordings preserved generative_font.ufo fontmakes test_cairo.pdf test/ignorables/ examples/*.pdf examples/*.otf examples/*.woff2 examples/fonts/*.ufo *.blend1 *.blend examples/blender/blends examples/blender/fonts profile.profile profile_result deps.txt _coldtype_notebook_tmp .ipynb_checkpoints _GoogleFonts _DownloadedFonts _site coldtype/demo/glfw-* ================================================ FILE: .python-version ================================================ 3.13 ================================================ FILE: CHANGELOG.md ================================================ # Changelog Starting at 0.5.0, all notable changes to Coldtype will be described here (briefly). Edit: looks like I forgot this existed, so we're starting again at 0.5.16 ## [0.5.0] - 2021-06-02 ### Added - `coldtype.fx.skia` - `SkiaImage` (subclass of `DATImage`, which has been moved to `coldtype.img` module) ### Removed - `.phototype`/`.color_phototype` methods on `DATPen` — these are now "chainable" methods in the _coldtype.fx.skia_ module, and can be applied by importing ala `from coldtype.fx.skia import phototype` and then chaining to a pen, `.ch(phototype(...))` ## [0.5.16] - 2021-08-03 ### Added - Minor improvements to the self-rasterizing/drawbot-renderer, to support transparent backgrounds ### Removed - `@drawbot_script` and `@drawbot_animation` from the global import; now reside and can be imported from `coldtype.drawbot` module ## [0.5.17] - 2021-08-06 ### Fixed - Quoting paths in `blend_frame` ### Removed - `unicodedata2` from primary installation requirements, in order to make blender installation smoother (i.e. not require Include header-copying from python source tarball) — thanks @colinmford! ## [0.6.0] - 2021-08-30 ### Added - New features for managing meshes in BlenderPen - embedded profiles, so `-p b3d` should now work globally ### Removed - `duration` keyword for `@animation`, since it’s redundant to `timeline=` shortcut ## [0.6.1] - 2021-08-31 ### Added - Support for `kp` and `tu` in `Glyphwise` ## [0.6.2] - 2021-09-01 ### Added - Support for single-char `Glyphwise` - `Glyphwise` now returns `DATPens`, not `DraftingPens` ## [0.6.3] - 2021-09-07 ### Added - Support for multi-return styler fn for `Glyphwise` - Coldtype panel for `@b3d_sequencer` ## [0.6.6] - 2021-09-10 ### Added - Better MIDI primitives - `coldtype midi` midi viewer ## [0.6.8] - 2021-09-16 ### Added - Better axidraw primitives in new `coldtype.axidraw` namespace, demonstrated in `test/visuals/test_axidraw.png` - New `numpad` special variable that can handle up to 9 special actions, triggerable from the numpad with the viewer enabled ### Removed - Dependency on `PyOpenGL-accelerate` in the `[viewer]` extra — not really sure why that was there to begin with. ## [0.6.9] - 2021-09-20 ### Added - Restored some audio capabilities, via `audio=` keywords and the new `ConfigOption.EnableAudio`/`KeyboardShortcut.EnableAudio` ## [0.7.0] - 2021-09-26 ### Fixed - General tidying, particularly for use in `[notebook]` ### Removed - Dependency on `noise` in `[viewer]` ## [0.7.1] - 2021-09-28 ### Added - Support for configurable `BLENDER_APP_PATH` - Better support for Windows-Blender workflow ## [0.7.2] - 2021-09-29 ### Added - Support for gif export in `@notebook_animation.show` ## [0.7.3] - 2021-10-07 ### Added - `strip=` kwarg on StSt (defaulting to `True`) so incoming text is automatically stripped (but can be overriden as in the `Glyphwise` use of `StSt`) - `coldtype.blender.fluent` experimental chainable interface for direct manipulation of blender objects ## [0.7.4] - 2021-10-21 ### Added - Support for lineTo's in `distribute_on_path` - More `coldtype.blender.fluent` interface - Ability to exit from renderer with -1 in `prenormalize_filepath` ## [0.7.5] - 2021-10-25 ### Added - `@ui` decorator idea, to be used/tested in Goodhertz plugin-builder ## [0.7.6] - 2021-11-02 ### Added - `.depth`, `.split`, `.wordPens`, `.walkp` - `î` and `ï` for `index` and `indices`, also both of those on `DraftingPen` now, since they shadow functionality of `mod_contour` and `map_points` - `utag` in walk.data - camelCase throughout examples, headed towards standardizing on that - `.geti` for time-based fetch in `AsciiTimeline` ### Fixed - `style=` on precomposed/@renderable-cached ## [0.8.0] - 2021-12-13 ### Added - `MidiTimeline` to replace `MidiReader` (`MidiTimeline` uses standard `Timeline` capabilities rather than Midi-specifi classes) - `AsciiTimeline` improvements and changed api, old `[]`-style access replaced by `.ki`; keyframe support also added, via `.kf`; many new examples in `examples/animations/ascii_*` - `Easeable` class to encapsulate all easing functionality, i.e. a `Timeable` bound to a frame value, meaning frame values can now be implied on all `@animation`-bound timelines (via `Timeline.hold`) - Generic lyric-video sentence building on timelines, available contextually as `.words` on a timeline (example in `examples/animations/ascii_words.py`) (adapted from older `Sequence` class (not recommended for use), originally developed for lyric video animation in Premiere, though works better now with Blender) - Automatic timeline viewer, available with `-tv 1` command-line arg or toggleable with `V` key in viewer app - A lot more examples - Normalized behavior of `point=` handling in `.scale`/`.rotate`/`.skew` - Manual-drive `BlenderTimeline` ### Removed - Implicit `BlenderTimeline` on `@b3d_animation` and `@b3d_sequencer` - `.progress` method (in favor of new `Easeable` apis) ### Changed - `.e` now defaults to `loops=1`, rather than `loops=0` ## [0.8.1] - 2021-12-28 ### Fixed - Audio support via `pyaudio` ## [0.8.2] - 2022-01-10 ### Fixed - Error where windows can't watch non-existent file - Error where windows barfs on os.uname ## [0.9.0] - 2022-01-18 ### Added - `coldtype.runon.runon` abstraction for chained/fluent method calling on nested lists - `coldtype.runon.path` i.e. `P` as a drop-in replacement for `DATPen/DATPens` (should be fully backwards compatible), which extends `coldtype.runon.runon` ### Fixed (maybe) - File watching on windows ### Removed - `watchdog` dependency (using `stat().st_mtime` polling instead now since we’re already running an event loop out of necessity for the viewer) ## [0.9.1] - 2022-01-31 ### Added - `memory=` keyword for renderables (attempting some kind of support for processing-style live-coded animations) - `fvar_` generic style for addressing sorted variable font axes - `x` key for xray mode - `g` key for grid mode - `p` key to print renderable content ## [0.9.2] - 2022-02-21 ### Added - support for platform-specific config files, `.coldtype.mac.py`, `.coldtype.win.py`, `.coldtype.lin.py` - minor Blender improvements for 3D workflow ## [0.9.3] - 2022-03-08 ### Added - `C` as valid alias for `CX` on xalign ### Fixed - `filterContours` copy before modify - Orphan-deletion in Blender ## [0.9.5] - 2022-03-24 ### Fixed - `bake=True` for a `@b3d_animation` ## [0.9.6] - 2022-07-13 ### Fixed - Spelling mistakes [h/t @HaydenBL] ### Added - Tons of experimental additional functionality for more direct-style scripting of blender and forthcoming coldtype-based blender typography addon (all in fluent.py, with some supporting tweaks in other coldtyper.blender infrastructure) ## [0.9.7] - 2022-08-06 ### Added - New transparency background when `bg` not specified - `render_bg` now defaults to `True`, since that seems to be the most common use-case - `ufo2ft` in `[blender]` optional requirements - simple `gifski` wrapper importable from `coldtyper.renderable.animation` ### Fixed - Better error message when trying to "release" via ffmpeg but no files have been rendered ## [0.9.8] - 2022-08-25 ### Added - New arg `round_result` on `SkiaImage.align`, to fix jaggy image aligning; defaults to True but is disableable ## [0.9.10] - 2022-09-25 ### Fixed - `glyph_to_uni` in packaged mode ## [0.9.11] - 2022-10-27 ### Added - `Runon.attach` ### Fixed - Explicit include of ufoLib2 for blender ## [0.9.12] - 2022-11-02 ### Added - `P.spread` as horizontal-only counterpart to `P.stack` (paired with `P.track` and `P.lead` respectively, funny that they rhyme oppositely whoops) ### Fixed - Default `"."` in ALL_FONT_DIRS for linux (so colab notebooks search in their uploaded files by default) ## [0.9.13] - 2022-11-08 ### Added - `Easeable.ec` as easing-cumulative (to help with partial rotations, as in the new truchet animation examples, and examples/animations/ec.py) ## [0.9.14] - 2022-11-16 ### Added - `P.gridlayer`, `Runon.mapvch`, `Runon.mapvrc` ## [0.10.0] - 2023-01-15 ### Added - `.up` as canonical form of `.ups` - Argument-less boolean operation methods for plural `P` - Automatic _-prefixed versions of all P methods, to get clojure-style "phrase"-commenting - `VERSIONS=` support for macro-like reversioning of a single animation - `Font.LibraryFind` and `Font.LibraryList` to search font registry (mac-only, windows is excruciatingly difficult) - `Font.names` to get style and family name of font via fontTools ### Changed - `Mondrian` -> `Scaffold` - `th` & `tv` (true-horizontal & true-vertical) have been renamed to potential confusion (since `th` could be taken to mean `true-height`) — these are now `tx` and `ty` respectively ## [0.10.2] - 2023-01-17 ### Changed - The Blender integration now inlines your local venv into Blender, meaning you no longer need to install any python packages into Blender's embedded python. ## [0.10.4] - 2023-01-20 ### Added - `-rar` (`--render-and-release`) command-line option to render-and-release(-and-then-quit) ## [0.10.5] - 2023-03-22 ### Improved - `Scaffold.cssgrid` now supports arbitrary regex keys in a dictionary arg for targeting multiple children at once (shown in test_scaffold.py) ## [0.10.6] - 2023-03-23 ### Added - `Runon.path` and `Runon.match` (taken from `Scaffold`) as new generic features, to regex-match on nested/slashed tags (i.e. paths) ## [0.10.7] - 2023-04-18 ### Fixed - `.ufo` font-reading on Windows ## [0.10.8] - 2023-06-11 ### Fixed - Font issue with `coldtype demo` ## [0.10.9] - 2023-06-23 ### Added - `b3denv` requirement, to help get default blender app path ## [0.10.10] - 2023-06-30 ### Fixed - `b3denv` assumes Blender exists, which is wrong ## [0.10.11] - 2023-07-31 ### Added - `P.to_code` (for a Goodhertz project that used the legacy `DATPen(s).to_code`) ## [0.10.12] - 2023-07-31 ### Added - Experimental support for reading control changes (cc messages) from midi files, via `MidiTimeline.ci` method ## [0.10.13] - 2023-08-04 ### Added - CycleVersionForward/CycleVersionBackward - Support for _version.py-style sidecar versioning - Support for special `__initials__` function to allow setting state from source file ## [0.10.14] - 2023-09-21 ### Added - `Rect.contains`/`Rect.__contains__`/`in` operator for Rect - `E` and `W` supported on `pair_to_edges` - `<` as parent ref in `Scaffold.find` - Support `vert` as feature (without needing to specify `features=`) - `RestartCount` concept in renderer - `Font.Fontmake` for full font compilation to a tmp file ## [0.10.15] - 2024-03-27 ### Added - `coldtype.fx.skia.freeze` for in-memory "freezing" of vectors (to avoid recalculation) - `Runon.collapseonce` for shallow collapsing ## [0.10.16] - 2024-05-16 ### Added - Support for skia-python 87.6 on python3.12 ## [0.10.17] ### Added - Fix for >= m87 skia-python with glfw gl-version setting (https://www.glfw.org/faq#macos / https://github.com/kyamagu/skia-python/issues/214) ## [0.10.18] ### Added - Support for custom global hotkeys via `custom_hotkey` function in source files ## [0.10.19] - 2024-06-20 ### Fixed - High quality filter support for skia > m87 ## [0.10.20] - 2024-07-23 ### Fixed - Spec <=0.4.2 for python-bidi since 0.5.0 is beefed ## [0.10.21] - 2024-09-30 ### Added - `use_skia_pathops_draw=False` kwarg override for `P.removeOverlap` to make sure we don't get all-off-curve+None quadratics in situations where we don't know how to convert that to cubics (i.e. in Blender) ## [0.10.22] - 2024-10-07 ### Added - `Rect.fit_aspect` for easily getting an aspect inscribed by a rect - version lock on python-bidi ## [0.11.0] - 2024-12-12 Huge update, attempting to future proof things ### Added - External dependency on coldtype/fontgoggles fork (instead of out-of-date inlined fontgoggles fork) ### Fixed - Gonna be honest — fixed lots of stuff, should’ve been writing down what I was fixing! ## [0.11.1] - 2024-12-15 ### Fixed - Make sure coldtype.drawbot does not require skia-python (via incorrect import) - try/except for filmjitter in coldtype.raster - Don’t enable audio if audio can’t be enabled (print instructions on enabling audio instead) ## [0.11.2] - 2025-01-26 ### Added - `Font.instances` to get variable font instance information - `coldtype instances font=` tool - `ººBLENDERINGºº` variable to quickly know if your code is running in blender or as a blender-renderer - `P.trim_start` and `P.trim_end` to quickly drop points from either end of a curve ### Changed - `P.boxCurveTo` `factor=` keyword now expects float, not int ### Fixed - Updated coldtype-fontgoggles to latest to avoid Windows thinking it could do objc ## [0.11.3] - 2025-01-28 ### Added - Experimental support for limited keyboard layout remapping via command line -kl argument ## [0.11.4] - 2025-02-20 ### Added - `ViewerSoloFirst` and `ViewerSoloLast` shortcuts ### Changed - `VIEWER_SOLO` is now a config option, not a one-off cli arg in renderer ## [0.11.5] - 2025-03-04 ### Added - `set_709` on `animation.export`/`FFMPEGExport` (so you can use older `ffmpeg` releases to export animations) ## [0.11.6] - 2025-03-11 ### Added - better support for custom output with ct viewseq - experimental support for `Theme` class to set multiple color styles from a single object ### Fixed - blender venv-inlining now look for a "src" directory if it exists (to match new pyproject.toml-enforced directory structure) ## [0.12.0] - 2025-03-18 ### Changed - base dependencies for coldtype (i.e. coldtype installed without any extra) no longer support "extended" font formats (woff, ufo, etc.), though [viewer] extra does still provide support for these; mido requirement has also been moved out of base into viewer; the point of this is to reduce the complexity of a "minimal" coldtype installation (i.e. when embedded as a backend service in another python application or a Blender extension, e.g.) ## [0.12.1] - 2025-03-23 ### Changed - refinements to Coldtype 2D panel in Blender for timeline workflow ## [0.12.2] - 2025-03-31 ### Fixed - ability to set workarea in coldtype 2d panel (blender) - memory bug in harfbuzz bounding box calculation by downgrading to <0.47 ## [0.12.3] - 2026-03-16 ### Fixed - Update to b3denv for Blender 5 compatibility ### Changed - Use `showinfilemanager` for cross-platform showing-in-"finder" ### Added - `demoblender` in addition to to `demo` for `uv run coldtype demoblender -p b3dlo` ## [0.12.4] - 2026-03-17 ### Fixed - Blender 5 fcurves utility ## [0.13.0] ### Changed - `coldtype` is now a metapackage that wraps `coldtype-core`, meaning `"coldtype[viewer]"` is no longer necessary, you can just do `uv add coldtype` or `uvx coldtype .py` and it should work ## [0.13.3] ### Changed - Soundfiles are now opened with "r" instead of "r+" which I think was totally extraneous (h/t https://github.com/coldtype/coldtype/discussions/185) ## [0.13.4] ### Added - `<` and `>` support for `Rect` based on combo of `w` and `h` - `Font.metrics` for quickly-accessing somewhat normalized font metrics data - `chars` tool ### Changed - Moved source for glyphs.py to `tools` (as a new tools directory) ## [0.13.5] ### Fixed - `renderer/.coldtype.py` not included in package ================================================ FILE: LICENSE ================================================ Copyright 2020 — Goodhertz, Inc. --- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README_EXTRAS.md ================================================ ### MIDI Todo ### Global hotkeys Uses pynput, but on Mac permissions are a pain now: [helpful instructions](https://textexpander.com/kb/mac/textexpander-is-forcing-me-to-enable-access-for-assistive-devices-how-and-what-is-that/#:~:text=Go%20to%20the%20System%20Preferences,%E2%80%9CEnable%20for%20assistive%20devices.%E2%80%9D) ### Mouse-passthrough on glfw Actually specifying it might not be necessary? Seems to work on my machine ``` brew install glfw --HEAD PYGLFW_LIBRARY=/usr/local/Cellar/glfw/HEAD-0b9e48f/lib/libglfw.3.4.dylib ``` ### GCP PyTorch images seem to work nicely (`c2-deeplearning-pytorch-1-8-cu110-v20210512-debian-10`), not sure this is a real link: https://console.cloud.google.com/compute/imagesDetail/projects/ml-images/global/images/c2-deeplearning-pytorch-1-8-cu110-v20210512-debian-10?folder=&organizationId=&project=uplifted-sol-90414 https://console.cloud.google.com/compute/imagesDetail/projects/ml-images/global/images/c2-deeplearning-pytorch-1-8-cu110-v20210512-debian-10 SSH w/ cloud console, then: - `sudo apt install libgl1-mesa-glx` # https://github.com/conda-forge/pygridgen-feedstock/issues/10 - `git clone https://github.com/goodhertz/coldtype` - `cd coldtype` - `pip install -e .` - `coldtype examples/animations/house.py -a -mp -cpu -ns` # -tc 32 (if it’s a big 32-instance) ### Second Monitor `coldtype examples/simplest.py -mn list` should print out names of monitors, then you match then (can be a substring) like this: `coldtype examples/simplest.py -mn SAM -wcs 1.0 -wp C` ### Installing with extras directly from git `pip install git+https://github.com/goodhertz/coldtype#egg=coldtype[viewer,experimental]` ================================================ FILE: assets/.gitignore ================================================ Noto* Twemoji* Source* ================================================ FILE: assets/ColdtypeObviously.designspace ================================================ ================================================ FILE: assets/ColdtypeObviously_BlackItalic.ufo/fontinfo.plist ================================================ ascender 815 capHeight 750 descender -185 familyName Coldtype Obviously guidelines italicAngle -14.0 openTypeNameDesigner James Edmondson openTypeNameDesignerURL http://ohnotype.co openTypeNameManufacturer OH no Type Company openTypeNameManufacturerURL http://ohnotype.co openTypeOS2Panose 4 2 1 5 4 1 2 2 1 0 openTypeOS2UnicodeRanges 0 1 2 openTypeOS2VendorID OHNO openTypeOS2WeightClass 800 openTypeOS2WidthClass 5 postscriptBlueFuzz 0 postscriptBlueScale 0.046875 postscriptBlueShift 7 postscriptBlueValues -14 2 660 673 747 763 774 789 postscriptFamilyBlues -14 2 604 618 750 763 772 784 postscriptFamilyOtherBlues -153 -134 postscriptFontName Obviously-Black postscriptForceBold postscriptOtherBlues -139 -120 postscriptStemSnapH 262 333 postscriptStemSnapV 310 337 postscriptWeightName Extra-bold styleName Black Italic unitsPerEm 1000 versionMajor 0 versionMinor 0 xHeight 660 ================================================ FILE: assets/ColdtypeObviously_BlackItalic.ufo/glyphs/C_.glif ================================================ public.markColor 0.9529,0.9294,0.5725,1 ================================================ FILE: assets/ColdtypeObviously_BlackItalic.ufo/glyphs/D_.glif ================================================ public.markColor 0.9529,0.9294,0.5725,1 ================================================ FILE: assets/ColdtypeObviously_BlackItalic.ufo/glyphs/E_.glif ================================================ public.markColor 0.9529,0.9294,0.5725,1 ================================================ FILE: assets/ColdtypeObviously_BlackItalic.ufo/glyphs/L_.glif ================================================ public.markColor 0.9529,0.9294,0.5725,1 ================================================ FILE: assets/ColdtypeObviously_BlackItalic.ufo/glyphs/O_.glif ================================================ public.markColor 0.9529,0.9294,0.5725,1 ================================================ FILE: assets/ColdtypeObviously_BlackItalic.ufo/glyphs/P_.glif ================================================ public.markColor 0.9529,0.9294,0.5725,1 ================================================ FILE: assets/ColdtypeObviously_BlackItalic.ufo/glyphs/T_.glif ================================================ public.markColor 0.9529,0.9294,0.5725,1 ================================================ FILE: assets/ColdtypeObviously_BlackItalic.ufo/glyphs/Y_.glif ================================================ public.markColor 0.9529,0.9294,0.5725,1 ================================================ FILE: assets/ColdtypeObviously_BlackItalic.ufo/glyphs/contents.plist ================================================ C C_.glif D D_.glif E E_.glif L L_.glif O O_.glif P P_.glif T T_.glif Y Y_.glif space space.glif ================================================ FILE: assets/ColdtypeObviously_BlackItalic.ufo/glyphs/layerinfo.plist ================================================ color 1,0.75,0,0.7 ================================================ FILE: assets/ColdtypeObviously_BlackItalic.ufo/glyphs/space.glif ================================================ public.markColor 0.5725,0.9294,0.9529,1 ================================================ FILE: assets/ColdtypeObviously_BlackItalic.ufo/groups.plist ================================================ public.kern1.A A Aacute Abreve Acircumflex Adieresis Agrave Amacron Aogonek Aring Aringacute Atilde public.kern1.AE AE AEacute E Eacute Ebreve Ecaron Ecircumflex Edieresis Edotaccent Egrave Emacron Eogonek OE public.kern1.C C Cacute Ccaron Ccedilla Ccircumflex Cdotaccent public.kern1.C.alt C Cacute.alt Ccaron.alt Ccedilla.alt Ccircumflex.alt Cdotaccent.alt public.kern1.D D Dcaron Dcroat Eth nine nine.alt public.kern1.Eng Eng N Nacute Ncaron Ncommaaccent Ntilde public.kern1.G G Gbreve Gcircumflex Gcommaaccent Gdotaccent public.kern1.G.alt Gbreve.alt Gcircumflex.alt Gcommaaccent.alt Gdotaccent.alt G.alt public.kern1.H H Hbar Hcircumflex I Iacute Ibreve Icircumflex Idieresis Idotaccent Igrave Imacron Iogonek Itilde M public.kern1.IJ IJ J Jcircumflex public.kern1.K K Kcommaaccent public.kern1.L L Lacute Lcommaaccent Lslash public.kern1.O O Oacute Obreve Ocircumflex Odieresis Ograve Ohungarumlaut Omacron Otilde zero eth public.kern1.Oslash Oslash Oslashacute public.kern1.R R Racute Rcaron Rcommaaccent public.kern1.S S Sacute Scaron Scedilla Scircumflex Scommaaccent dollar public.kern1.S.alt Sacute.alt Scaron.alt Scedilla.alt Scircumflex.alt Scommaaccent.alt S.alt dollar.alt public.kern1.T T Tbar Tcaron Tcommaaccent uni021A public.kern1.U U Uacute Ubreve Ucircumflex Udieresis Ugrave Uhungarumlaut Umacron Uogonek Uring Utilde public.kern1.W W Wacute Wcircumflex Wdieresis Wgrave public.kern1.Y Y Yacute Ycircumflex Ydieresis public.kern1.Z Z Zacute Zcaron Zdotaccent public.kern1.a a.alt2 aacute.alt2 abreve.alt2 acircumflex.alt2 adieresis.alt2 agrave.alt2 amacron.alt2 aogonek.alt2 aring.alt2 aringacute.alt2 atilde.alt2 aacute.alt abreve.alt acircumflex.alt adieresis.alt agrave.alt amacron.alt aogonek.alt aring.alt aringacute.alt atilde.alt a.alt public.kern1.ae ae.alt2 aeacute.alt2 e eacute ebreve ecaron ecircumflex edieresis edotaccent egrave emacron eogonek oe ae aeacute public.kern1.afii57929 quotedblright quoteright public.kern1.at at at.case public.kern1.b b p thorn public.kern1.braceleft braceleft bracketleft public.kern1.braceright braceright bracketright public.kern1.c c cacute ccaron ccedilla ccircumflex cdotaccent public.kern1.c.alt cacute.alt ccaron.alt ccedilla.alt ccircumflex.alt cdotaccent.alt c.alt public.kern1.colon colon semicolon public.kern1.comma comma ellipsis period quotedblbase quotesinglbase public.kern1.copyright copyright copyright.alt registered public.kern1.d d dcroat fl l lacute lcommaaccent lslash public.kern1.dcaron dcaron lcaron public.kern1.dotlessi dotlessi fi i iacute ibreve icircumflex idieresis igrave imacron iogonek itilde aacute abreve acircumflex adieresis agrave amacron aogonek aring aringacute atilde public.kern1.e.alt eacute.alt ebreve.alt ecaron.alt ecircumflex.alt edieresis.alt edotaccent.alt egrave.alt emacron.alt eogonek.alt ae.alt e.alt public.kern1.eight eight three three.alt B germandbls public.kern1.emdash emdash endash hyphen public.kern1.eng eng ij j y.alt g.alt g gcommaaccent.alt gcommaaccent gbreve.alt gcircumflex.alt gdotaccent.alt yacute.alt ycircumflex.alt ydieresis.alt gbreve gcircumflex gdotaccent yacute ycircumflex ydieresis q jcircumflex public.kern1.five five five.alt public.kern1.five.osf five.osf five.osf.alt public.kern1.g g.alt2 gbreve.alt2 gcircumflex.alt2 gcommaaccent.alt2 gdotaccent.alt2 public.kern1.guillemotleft guillemotleft guilsinglleft public.kern1.guillemotleft.case guilsinglleft.case guillemotleft.case public.kern1.guillemotright guillemotright guilsinglright public.kern1.guillemotright.case guillemotright.case guilsinglright.case public.kern1.h h hbar hcircumflex m n nacute napostrophe ncaron ncommaaccent ntilde public.kern1.hyphen.case emdash.case endash.case hyphen.case plus minus public.kern1.k k kcommaaccent kgreenlandic public.kern1.minute quotedbl quotesingle public.kern1.nine.osf nine.osf nine.osf.alt public.kern1.o o oacute obreve ocircumflex odieresis ograve ohungarumlaut omacron otilde public.kern1.oslash oslash oslashacute public.kern1.question question question.alt public.kern1.quotedblleft quotedblleft quoteleft public.kern1.r r racute rcaron rcommaaccent public.kern1.s s sacute scaron scedilla scircumflex scommaaccent public.kern1.s.alt sacute.alt scaron.alt scedilla.alt scircumflex.alt scommaaccent.alt s.alt public.kern1.t t tbar tcaron tcommaaccent uni021B public.kern1.t.alt tcaron.alt tcommaaccent.alt t.alt uni021B.alt public.kern1.t.alt2 tcaron.alt2 tcommaaccent.alt2 t.alt2 uni021B.alt2 public.kern1.three.osf three.osf three.osf.alt public.kern1.two two two.alt public.kern1.two.osf two.osf two.osf.alt public.kern1.u u uacute ubreve ucircumflex udieresis ugrave uhungarumlaut umacron uogonek uring utilde public.kern1.v v w wacute wcircumflex wdieresis wgrave public.kern1.y y.alt2 yacute.alt2 ycircumflex.alt2 ydieresis.alt2 public.kern1.z z zacute zcaron zdotaccent public.kern2.A A Aacute Abreve Acircumflex Adieresis Agrave Amacron Aogonek Aring Aringacute Atilde public.kern2.AE AE AEacute public.kern2.B B D Dcaron Dcroat E Eacute Ebreve Ecaron Ecircumflex Edieresis Edotaccent Egrave Emacron Eogonek Eth F H Hbar Hcircumflex I IJ Iacute Ibreve Icircumflex Idieresis Idotaccent Igrave Imacron Iogonek Itilde K Kcommaaccent L Lacute Lcaron Lcommaaccent Ldot Lslash P R Racute Rcaron Rcommaaccent Thorn one uni1E9E public.kern2.C C Cacute Ccaron Ccedilla Ccircumflex Cdotaccent G Gbreve Gcircumflex Gcommaaccent Gdotaccent O OE Oacute Obreve Ocircumflex Odieresis Ograve Ohungarumlaut Omacron Otilde Q C G.alt zero six six.alt Cacute.alt Ccaron.alt Ccedilla.alt Ccircumflex.alt Cdotaccent.alt Gbreve.alt Gcircumflex.alt Gcommaaccent.alt Gdotaccent.alt public.kern2.Eng Eng M N Nacute Ncaron Ncommaaccent Ntilde afii61352 public.kern2.J J Jcircumflex public.kern2.Oslash Oslashacute Oslash public.kern2.S S Sacute Scaron Scedilla Scircumflex Scommaaccent dollar public.kern2.S.alt Sacute.alt Scaron.alt Scedilla.alt Scircumflex.alt Scommaaccent.alt S.alt dollar.alt public.kern2.T T Tbar Tcaron Tcommaaccent uni021A public.kern2.U U Uacute Ubreve Ucircumflex Udieresis Ugrave Uhungarumlaut Umacron Uogonek Uring Utilde public.kern2.W W Wacute Wcircumflex Wdieresis Wgrave public.kern2.Y Y Yacute Ycircumflex Ydieresis public.kern2.Z Z Zacute Zcaron Zdotaccent public.kern2.a a.alt2 aacute.alt2 abreve.alt2 acircumflex.alt2 adieresis.alt2 ae.alt2 aeacute.alt2 agrave.alt2 amacron.alt2 aogonek.alt2 aring.alt2 aringacute.alt2 atilde.alt2 public.kern2.a.alt aacute.alt abreve.alt acircumflex.alt adieresis.alt aeacute.alt agrave.alt amacron.alt aogonek.alt aring.alt aringacute.alt atilde.alt a.alt ae.alt public.kern2.at at at.case public.kern2.b b thorn public.kern2.braceleft braceleft bracketleft public.kern2.braceright braceright bracketright public.kern2.c c cacute ccaron ccedilla ccircumflex cdotaccent d dcaron dcroat e eacute ebreve ecaron ecircumflex edieresis edotaccent egrave emacron eogonek eth o oacute obreve ocircumflex odieresis oe ograve ohungarumlaut omacron oslash oslashacute otilde q a ae c.alt e.alt g.alt cacute.alt ccaron.alt ccedilla.alt ccircumflex.alt cdotaccent.alt eacute.alt ebreve.alt ecaron.alt ecircumflex.alt edieresis.alt edotaccent.alt egrave.alt emacron.alt eogonek.alt gbreve.alt gcircumflex.alt gdotaccent.alt aacute abreve acircumflex adieresis aeacute agrave amacron aogonek aring aringacute atilde gbreve gcircumflex gdotaccent gcommaaccent.alt public.kern2.colon colon semicolon public.kern2.comma comma ellipsis period quotedblbase quotesinglbase public.kern2.copyright copyright copyright.alt registered public.kern2.dotlessi dotlessi i iacute ibreve icircumflex idieresis igrave ij imacron iogonek itilde p eng public.kern2.emdash emdash endash hyphen public.kern2.eng kgreenlandic m n nacute ncaron ncommaaccent ntilde r racute rcaron rcommaaccent public.kern2.f f fi fl public.kern2.g g.alt2 gbreve.alt2 gcircumflex.alt2 gcommaaccent.alt2 gdotaccent.alt2 public.kern2.guillemotleft guillemotleft guilsinglleft public.kern2.guillemotleft.case guillemotleft.case guilsinglleft.case public.kern2.guillemotright guillemotright guilsinglright public.kern2.guillemotright.case guillemotright.case guilsinglright.case public.kern2.h h hbar hcircumflex k kcommaaccent l lacute lcaron lcommaaccent ldot lslash germandbls public.kern2.hyphen.case emdash.case endash.case hyphen.case plus minus public.kern2.j j public.kern2.minute quotedbl quotesingle public.kern2.onesuperior onesuperior threesuperior twosuperior public.kern2.quotedblleft quotedblleft quoteleft public.kern2.quoteright quotedblright quoteright public.kern2.s s sacute scaron scedilla scircumflex scommaaccent public.kern2.s.alt sacute.alt scaron.alt scedilla.alt scircumflex.alt scommaaccent.alt s.alt public.kern2.t t tbar tcaron tcommaaccent uni021B public.kern2.t.alt tcaron.alt tcommaaccent.alt t.alt uni021B.alt public.kern2.t.alt2 tcaron.alt2 tcommaaccent.alt2 t.alt2 uni021B.alt2 public.kern2.u u uacute ubreve ucircumflex udieresis ugrave uhungarumlaut umacron uogonek uring utilde yacute.alt ycircumflex.alt ydieresis.alt y.alt y yacute ycircumflex ydieresis public.kern2.v v w wacute wcircumflex wdieresis wgrave public.kern2.y y.alt2 yacute.alt2 ycircumflex.alt2 ydieresis.alt2 public.kern2.z z zacute zcaron zdotaccent ================================================ FILE: assets/ColdtypeObviously_BlackItalic.ufo/layercontents.plist ================================================ foreground glyphs ================================================ FILE: assets/ColdtypeObviously_BlackItalic.ufo/lib.plist ================================================ com.defcon.sortDescriptor ascending .notdef A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z Aacute aacute Abreve abreve Acircumflex acircumflex Adieresis adieresis Agrave agrave Amacron amacron Aogonek aogonek Aring aring Aringacute aringacute Atilde atilde AE ae AEacute aeacute Cacute cacute Ccaron ccaron Ccedilla ccedilla Ccircumflex ccircumflex Cdotaccent cdotaccent Dcaron dcaron Dcroat dcroat Eacute eacute Ebreve ebreve Ecaron ecaron Ecircumflex ecircumflex Edieresis edieresis Edotaccent edotaccent Egrave egrave Emacron emacron Eng eng Eogonek eogonek Eth eth fi fl Gbreve gbreve Gcircumflex gcircumflex Gcommaaccent gcommaaccent Gdotaccent gdotaccent Hbar hbar Hcircumflex hcircumflex Iacute iacute Ibreve ibreve Icircumflex icircumflex Idieresis idieresis Idotaccent Igrave igrave IJ ij Imacron imacron Iogonek iogonek Itilde itilde dotlessi Jcircumflex jcircumflex Kcommaaccent kcommaaccent kgreenlandic Lacute lacute Lcaron lcaron Lcommaaccent lcommaaccent Ldot ldot Lslash lslash Nacute nacute napostrophe Ncaron ncaron Ncommaaccent ncommaaccent Ntilde ntilde Oacute oacute Obreve obreve Ocircumflex ocircumflex Odieresis odieresis Ograve ograve Ohungarumlaut ohungarumlaut Omacron omacron Oslash oslash Oslashacute oslashacute Otilde otilde OE oe Racute racute Rcaron rcaron Rcommaaccent rcommaaccent Sacute sacute Scaron scaron Scedilla scedilla Scircumflex scircumflex Scommaaccent scommaaccent Tbar tbar Tcaron tcaron Tcommaaccent tcommaaccent uni021A uni021B Thorn thorn Uacute uacute Ubreve ubreve Ucircumflex ucircumflex Udieresis udieresis Ugrave ugrave Uhungarumlaut uhungarumlaut Umacron umacron Uogonek uogonek Uring uring Utilde utilde Wacute wacute Wcircumflex wcircumflex Wdieresis wdieresis Wgrave wgrave Yacute yacute Ycircumflex ycircumflex Ydieresis ydieresis Zacute zacute Zcaron zcaron Zdotaccent zdotaccent uni1E9E germandbls C.alt Ccedilla.alt Cacute.alt Ccaron.alt Ccircumflex.alt Cdotaccent.alt G.alt Gbreve.alt Gcircumflex.alt Gcommaaccent.alt Gdotaccent.alt S.alt Sacute.alt Scaron.alt Scedilla.alt Scircumflex.alt Scommaaccent.alt a.alt agrave.alt acircumflex.alt aacute.alt abreve.alt amacron.alt aogonek.alt aring.alt aringacute.alt atilde.alt adieresis.alt ae.alt aeacute.alt c.alt ccedilla.alt cacute.alt ccaron.alt ccircumflex.alt cdotaccent.alt e.alt eacute.alt egrave.alt ecircumflex.alt edieresis.alt ebreve.alt ecaron.alt edotaccent.alt emacron.alt eogonek.alt a.alt2 agrave.alt2 acircumflex.alt2 adieresis.alt2 aacute.alt2 abreve.alt2 amacron.alt2 aogonek.alt2 aring.alt2 aringacute.alt2 atilde.alt2 ae.alt2 aeacute.alt2 g.alt gbreve.alt gcircumflex.alt gdotaccent.alt gcommaaccent.alt g.alt2 gbreve.alt2 gcircumflex.alt2 gdotaccent.alt2 gcommaaccent.alt2 s.alt sacute.alt scaron.alt scedilla.alt scircumflex.alt scommaaccent.alt t.alt tbar.alt tcaron.alt tcommaaccent.alt uni021B.alt t.alt2 tbar.alt2 tcaron.alt2 tcommaaccent.alt2 uni021B.alt2 y.alt yacute.alt ycircumflex.alt ydieresis.alt y.alt2 yacute.alt2 ycircumflex.alt2 ydieresis.alt2 zero one two three four five six seven eight nine two.alt three.alt five.alt six.alt nine.alt period comma colon semicolon ellipsis periodcentered periodcentered.case bullet bullet.case ampersand ampersand.alt exclam exclamdown exclamdown.case question question.alt questiondown questiondown.alt questiondown.case questiondown.alt.case quotedblleft quotedblright quoteleft quoteright quotedblbase quotesinglbase guillemotleft guillemotright guillemotleft.case guillemotright.case guilsinglleft guilsinglright guilsinglleft.case guilsinglright.case hyphen endash emdash hyphen.case endash.case emdash.case underscore slash backslash bar brokenbar parenleft parenright parenleft.case parenright.case bracketleft bracketright bracketleft.case bracketright.case braceleft braceright braceleft.case braceright.case at at.case copyright copyright.alt registered trademark asterisk dagger daggerdbl asciicircum asciitilde dollar dollar.alt cent cent.alt sterling sterling.alt yen Euro Euro.alt numbersign paragraph section section.alt ordfeminine ordfeminine.alt ordfeminine.alt2 ordmasculine degree percent perthousand quotedbl quotesingle plus minus less greater equal multiply divide zero.osf one.osf two.osf three.osf four.osf five.osf six.osf seven.osf eight.osf nine.osf two.osf.alt three.osf.alt five.osf.alt six.osf.alt nine.osf.alt dollar.osf sterling.osf Euro.osf yen.osf dollar.osf.alt sterling.osf.alt Euro.osf.alt acute hungarumlaut caron.alt grave circumflex caron breve tilde macron dieresis dotaccent ring commaaccent cedilla ogonek baraccent afii61352 onesuperior twosuperior threesuperior onequarter onehalf threequarters onehalf.alt threequarters.alt fraction zero.numr one.numr two.numr three.numr four.numr five.numr six.numr seven.numr eight.numr nine.numr two.alt.numr three.alt.numr five.alt.numr six.alt.numr nine.alt.numr zero.dnom one.dnom two.dnom three.dnom four.dnom five.dnom six.dnom seven.dnom eight.dnom nine.dnom two.alt.dnom three.alt.dnom five.alt.dnom six.alt.dnom nine.alt.dnom arrowleft arrowup arrowright arrowdown filledbox space thinspace nbspace Aacute.compact type glyphList com.loicsander.scaleFast guides Height 802 Name New guide 1 presets com.typemytype.robofont.background.layerStrokeColor 1.0 0.75 0.0 0.7 com.typemytype.robofont.compileSettings.MacRomanFirst com.typemytype.robofont.compileSettings.autohint com.typemytype.robofont.compileSettings.checkOutlines 1 com.typemytype.robofont.compileSettings.createDummyDSIG com.typemytype.robofont.compileSettings.decompose 1 com.typemytype.robofont.compileSettings.generateFormat 0 com.typemytype.robofont.compileSettings.layerName foreground com.typemytype.robofont.compileSettings.path /Users/jamesedmondson/Dropbox/Ohno/Fonts/DevObviously_BlackItalic.otf com.typemytype.robofont.compileSettings.releaseMode com.typemytype.robofont.foreground.layerStrokeColor 0.5725 0.5725 0.9529 1.0 com.typemytype.robofont.guideline.magnetic.1jMNUxSUou 5.0 com.typemytype.robofont.guideline.magnetic.DUaMYzQSQM 5.0 com.typemytype.robofont.guideline.magnetic.QhhoCLTuHS 5.0 com.typemytype.robofont.guideline.magnetic.V9hiWVV33p 5.0 com.typemytype.robofont.guideline.magnetic.d3Nkv4XtBG 5.0 com.typemytype.robofont.guideline.magnetic.liIC7QtlqK 5.0 com.typemytype.robofont.guideline.showMeasurements.1jMNUxSUou com.typemytype.robofont.guideline.showMeasurements.DUaMYzQSQM com.typemytype.robofont.guideline.showMeasurements.QhhoCLTuHS com.typemytype.robofont.guideline.showMeasurements.V9hiWVV33p com.typemytype.robofont.guideline.showMeasurements.d3Nkv4XtBG com.typemytype.robofont.guideline.showMeasurements.liIC7QtlqK com.typemytype.robofont.italicSlantOffset 0 com.typemytype.robofont.segmentType curve com.typemytype.robofont.shouldAddPointsInSplineConversion 1 com.typemytype.robofont.smartSets.uniqueKey 4720224272 com.typesupply.defcon.sortDescriptor ascending .notdef A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z AE AEacute Aacute Abreve Acircumflex Adieresis Agrave Amacron Aogonek Aring Aringacute Atilde C.alt Cacute Ccaron Ccedilla Ccircumflex Cdotaccent Dcaron Dcroat Eacute Ebreve Ecaron Ecircumflex Edieresis Edotaccent Egrave Emacron Eng Eogonek Eth Euro Euro.osf G.alt Gbreve Gcircumflex Gcommaaccent Gdotaccent Hbar Hcircumflex IJ Iacute Ibreve Icircumflex Idieresis Idotaccent Igrave Imacron Iogonek Itilde Jcircumflex Kcommaaccent Lacute Lcaron Lcommaaccent Ldot Lslash Nacute Ncaron Ncommaaccent Ntilde OE Oacute Obreve Ocircumflex Odieresis Ograve Ohungarumlaut Omacron Oslash Oslashacute Otilde Racute Rcaron Rcommaaccent S.alt Sacute Scaron Scedilla Scircumflex Scommaaccent Tbar Tcaron Tcommaaccent Thorn Uacute Ubreve Ucircumflex Udieresis Ugrave Uhungarumlaut Umacron Uogonek Uring Utilde Wacute Wcircumflex Wdieresis Wgrave Yacute Ycircumflex Ydieresis Zacute Zcaron Zdotaccent a.alt aacute abreve acircumflex acute acute.case adieresis ae aeacute afii61352 agrave amacron ampersand ampersand.alt ampersand.ss01 aogonek aring aringacute arrowboth arrowdown arrowleft arrowright arrowup arrowupdn asciicircum asciitilde asterisk at at.case atilde backslash bar braceleft braceleft.case braceright braceright.case bracketleft bracketleft.case bracketright bracketright.case breve breve.case brokenbar bullet bullet.case c.alt cacute caron caron.alt caron.case ccaron ccedilla ccircumflex cdotaccent cedilla cent circumflex circumflex.case colon comma commaaccent copyright dagger daggerdbl dcaron dcroat degree dieresis dieresis.case divide dollar dollar.osf dotaccent dotaccent.case dotlessi e.alt eacute ebreve ecaron ecircumflex edieresis edotaccent egrave eight eight.dnom eight.numr eight.osf ellipsis emacron emdash emdash.case endash endash.case eng eogonek equal eth exclam exclamdown fi filledbox five five.alt five.dnom five.numr five.osf fl four four.dnom four.numr four.osf fraction g.alt gbreve gcircumflex gcommaaccent gdotaccent germandbls grave grave.case greater guillemotleft guillemotleft.case guillemotright guillemotright.case guilsinglleft guilsinglleft.case guilsinglright guilsinglright.case hbar hcircumflex hungarumlaut hungarumlaut.case hyphen hyphen.case iacute ibreve icircumflex idieresis igrave ij imacron iogonek itilde jcircumflex kcommaaccent kgreenlandic lacute lcaron lcommaaccent ldot less lslash macron macron.case minus multiply nacute napostrophe nbspace ncaron ncommaaccent nine nine.alt nine.dnom nine.numr nine.osf ntilde numbersign oacute obreve ocircumflex odieresis oe ogonek ograve ohungarumlaut omacron one one.dnom one.numr one.osf onehalf onequarter onesuperior ordfeminine ordmasculine oslash oslashacute otilde paragraph parenleft parenleft.case parenright parenright.case percent period periodcentered periodcentered.case perthousand plus question question.alt questiondown quotedbl quotedblbase quotedblleft quotedblright quoteleft quoteright quotesinglbase quotesingle racute rcaron rcommaaccent registered ring ring.case s.alt sacute scaron scedilla scircumflex scommaaccent section semicolon seven seven.dnom seven.numr seven.osf six six.alt six.dnom six.numr six.osf slash space sterling sterling.osf tbar tcaron tcommaaccent thinspace thorn three three.alt three.dnom three.numr three.osf threequarters threesuperior tilde tilde.case trademark two two.alt two.dnom two.numr two.osf twosuperior uacute ubreve ucircumflex udieresis ugrave uhungarumlaut umacron underscore uni021A uni021B uni1E9E uni2196 uni2197 uni2198 uni2199 uogonek uring utilde wacute wcircumflex wdieresis wgrave y.alt yacute ycircumflex ydieresis yen yen.osf zacute zcaron zdotaccent zero zero.dnom zero.numr zero.osf t.alt g.alt2 type glyphList public.glyphOrder D E L O P T Y C space ================================================ FILE: assets/ColdtypeObviously_BlackItalic.ufo/metainfo.plist ================================================ creator com.github.fonttools.ufoLib formatVersion 3 ================================================ FILE: assets/ColdtypeObviously_CompressedBlackItalic.ufo/fontinfo.plist ================================================ ascender 815 capHeight 750 descender -185 familyName Coldtype Obviously guidelines angle 0 identifier 6lmnulpnLT name descender x 174 y -104 italicAngle -14.0 openTypeNameDesigner James Edmondson openTypeNameDesignerURL http://ohnotype.co openTypeNameManufacturer OH no Type Company openTypeNameManufacturerURL http://ohnotype.co openTypeOS2Panose 4 2 1 5 4 1 2 2 1 0 openTypeOS2UnicodeRanges 0 1 2 openTypeOS2VendorID OHNO openTypeOS2WeightClass 800 openTypeOS2WidthClass 5 postscriptBlueFuzz 0 postscriptBlueScale 0.08333 postscriptBlueShift 7 postscriptBlueValues -10 0 658 667 749 758 774 779 postscriptFamilyBlues -14 2 604 618 750 763 772 784 postscriptFamilyOtherBlues -153 -134 postscriptForceBold postscriptOtherBlues -111 -103 postscriptStemSnapH 209 263 postscriptStemSnapV 86 95 styleName Compressed Black Italic unitsPerEm 1000 versionMajor 0 versionMinor 0 xHeight 660 ================================================ FILE: assets/ColdtypeObviously_CompressedBlackItalic.ufo/glyphs/C_.glif ================================================ public.markColor 0.9529,0.9294,0.5725,1 ================================================ FILE: assets/ColdtypeObviously_CompressedBlackItalic.ufo/glyphs/D_.glif ================================================ public.markColor 0.9529,0.9294,0.5725,1 ================================================ FILE: assets/ColdtypeObviously_CompressedBlackItalic.ufo/glyphs/E_.glif ================================================ public.markColor 0.9529,0.9294,0.5725,1 ================================================ FILE: assets/ColdtypeObviously_CompressedBlackItalic.ufo/glyphs/L_.glif ================================================ public.markColor 0.9529,0.9294,0.5725,1 ================================================ FILE: assets/ColdtypeObviously_CompressedBlackItalic.ufo/glyphs/O_.glif ================================================ public.markColor 0.9529,0.9294,0.5725,1 ================================================ FILE: assets/ColdtypeObviously_CompressedBlackItalic.ufo/glyphs/P_.glif ================================================ public.markColor 0.9529,0.9294,0.5725,1 ================================================ FILE: assets/ColdtypeObviously_CompressedBlackItalic.ufo/glyphs/T_.glif ================================================ public.markColor 0.9529,0.9294,0.5725,1 ================================================ FILE: assets/ColdtypeObviously_CompressedBlackItalic.ufo/glyphs/Y_.glif ================================================ public.markColor 0.9529,0.9294,0.5725,1 ================================================ FILE: assets/ColdtypeObviously_CompressedBlackItalic.ufo/glyphs/contents.plist ================================================ C C_.glif D D_.glif E E_.glif L L_.glif O O_.glif P P_.glif T T_.glif Y Y_.glif space space.glif ================================================ FILE: assets/ColdtypeObviously_CompressedBlackItalic.ufo/glyphs/layerinfo.plist ================================================ color 1,0.75,0,0.7 ================================================ FILE: assets/ColdtypeObviously_CompressedBlackItalic.ufo/glyphs/space.glif ================================================ public.markColor 0.5725,0.9294,0.9529,1 ================================================ FILE: assets/ColdtypeObviously_CompressedBlackItalic.ufo/groups.plist ================================================ public.kern1.A A Aacute Abreve Acircumflex Adieresis Agrave Amacron Aogonek Aring Aringacute Atilde public.kern1.AE AE AEacute E Eacute Ebreve Ecaron Ecircumflex Edieresis Edotaccent Egrave Emacron Eogonek OE public.kern1.C C Cacute Ccaron Ccedilla Ccircumflex Cdotaccent public.kern1.C.alt C Cacute.alt Ccaron.alt Ccedilla.alt Ccircumflex.alt Cdotaccent.alt public.kern1.D D Dcaron Dcroat Eth nine nine.alt public.kern1.Eng Eng N Nacute Ncaron Ncommaaccent Ntilde public.kern1.G G Gbreve Gcircumflex Gcommaaccent Gdotaccent public.kern1.G.alt Gbreve.alt Gcircumflex.alt Gcommaaccent.alt Gdotaccent.alt G.alt public.kern1.H H Hbar Hcircumflex I Iacute Ibreve Icircumflex Idieresis Idotaccent Igrave Imacron Iogonek Itilde M public.kern1.IJ IJ J Jcircumflex public.kern1.K K Kcommaaccent public.kern1.L L Lacute Lcommaaccent Lslash public.kern1.O O Oacute Obreve Ocircumflex Odieresis Ograve Ohungarumlaut Omacron Otilde zero eth public.kern1.Oslash Oslash Oslashacute public.kern1.R R Racute Rcaron Rcommaaccent public.kern1.S S Sacute Scaron Scedilla Scircumflex Scommaaccent dollar public.kern1.S.alt Sacute.alt Scaron.alt Scedilla.alt Scircumflex.alt Scommaaccent.alt S.alt dollar.alt public.kern1.T T Tbar Tcaron Tcommaaccent uni021A public.kern1.U U Uacute Ubreve Ucircumflex Udieresis Ugrave Uhungarumlaut Umacron Uogonek Uring Utilde public.kern1.W W Wacute Wcircumflex Wdieresis Wgrave public.kern1.Y Y Yacute Ycircumflex Ydieresis public.kern1.Z Z Zacute Zcaron Zdotaccent public.kern1.a a.alt2 aacute.alt2 abreve.alt2 acircumflex.alt2 adieresis.alt2 agrave.alt2 amacron.alt2 aogonek.alt2 aring.alt2 aringacute.alt2 atilde.alt2 aacute.alt abreve.alt acircumflex.alt adieresis.alt agrave.alt amacron.alt aogonek.alt aring.alt aringacute.alt atilde.alt a.alt public.kern1.ae ae.alt2 aeacute.alt2 e eacute ebreve ecaron ecircumflex edieresis edotaccent egrave emacron eogonek oe ae aeacute public.kern1.afii57929 quotedblright quoteright public.kern1.at at at.case public.kern1.b b p thorn public.kern1.braceleft braceleft bracketleft public.kern1.braceright braceright bracketright public.kern1.c c cacute ccaron ccedilla ccircumflex cdotaccent public.kern1.c.alt cacute.alt ccaron.alt ccedilla.alt ccircumflex.alt cdotaccent.alt c.alt public.kern1.colon colon semicolon public.kern1.comma comma ellipsis period quotedblbase quotesinglbase public.kern1.copyright copyright copyright.alt registered public.kern1.d d dcroat fl l lacute lcommaaccent lslash public.kern1.dcaron dcaron lcaron public.kern1.dotlessi dotlessi fi i iacute ibreve icircumflex idieresis igrave imacron iogonek itilde aacute abreve acircumflex adieresis agrave amacron aogonek aring aringacute atilde public.kern1.e.alt eacute.alt ebreve.alt ecaron.alt ecircumflex.alt edieresis.alt edotaccent.alt egrave.alt emacron.alt eogonek.alt ae.alt e.alt public.kern1.eight eight three three.alt B germandbls public.kern1.emdash emdash endash hyphen public.kern1.eng eng ij j y.alt g.alt g gcommaaccent.alt gcommaaccent gbreve.alt gcircumflex.alt gdotaccent.alt yacute.alt ycircumflex.alt ydieresis.alt gbreve gcircumflex gdotaccent yacute ycircumflex ydieresis q jcircumflex public.kern1.five five five.alt public.kern1.five.osf five.osf five.osf.alt public.kern1.g g.alt2 gbreve.alt2 gcircumflex.alt2 gcommaaccent.alt2 gdotaccent.alt2 public.kern1.guillemotleft guillemotleft guilsinglleft public.kern1.guillemotleft.case guilsinglleft.case guillemotleft.case public.kern1.guillemotright guillemotright guilsinglright public.kern1.guillemotright.case guillemotright.case guilsinglright.case public.kern1.h h hbar hcircumflex m n nacute napostrophe ncaron ncommaaccent ntilde public.kern1.hyphen.case emdash.case endash.case hyphen.case plus minus public.kern1.k k kcommaaccent kgreenlandic public.kern1.minute quotedbl quotesingle public.kern1.nine.osf nine.osf nine.osf.alt public.kern1.o o oacute obreve ocircumflex odieresis ograve ohungarumlaut omacron otilde public.kern1.oslash oslash oslashacute public.kern1.question question question.alt public.kern1.quotedblleft quotedblleft quoteleft public.kern1.r r racute rcaron rcommaaccent public.kern1.s s sacute scaron scedilla scircumflex scommaaccent public.kern1.s.alt sacute.alt scaron.alt scedilla.alt scircumflex.alt scommaaccent.alt s.alt public.kern1.t t tbar tcaron tcommaaccent uni021B public.kern1.t.alt tcaron.alt tcommaaccent.alt t.alt uni021B.alt public.kern1.t.alt2 tcaron.alt2 tcommaaccent.alt2 t.alt2 uni021B.alt2 public.kern1.three.osf three.osf three.osf.alt public.kern1.two two two.alt public.kern1.two.osf two.osf two.osf.alt public.kern1.u u uacute ubreve ucircumflex udieresis ugrave uhungarumlaut umacron uogonek uring utilde public.kern1.v v w wacute wcircumflex wdieresis wgrave public.kern1.y y.alt2 yacute.alt2 ycircumflex.alt2 ydieresis.alt2 public.kern1.z z zacute zcaron zdotaccent public.kern2.A A Aacute Abreve Acircumflex Adieresis Agrave Amacron Aogonek Aring Aringacute Atilde public.kern2.AE AE AEacute public.kern2.B B D Dcaron Dcroat E Eacute Ebreve Ecaron Ecircumflex Edieresis Edotaccent Egrave Emacron Eogonek Eth F H Hbar Hcircumflex I IJ Iacute Ibreve Icircumflex Idieresis Idotaccent Igrave Imacron Iogonek Itilde K Kcommaaccent L Lacute Lcaron Lcommaaccent Ldot Lslash P R Racute Rcaron Rcommaaccent Thorn one uni1E9E public.kern2.C C Cacute Ccaron Ccedilla Ccircumflex Cdotaccent G Gbreve Gcircumflex Gcommaaccent Gdotaccent O OE Oacute Obreve Ocircumflex Odieresis Ograve Ohungarumlaut Omacron Otilde Q C G.alt zero six six.alt Cacute.alt Ccaron.alt Ccedilla.alt Ccircumflex.alt Cdotaccent.alt Gbreve.alt Gcircumflex.alt Gcommaaccent.alt Gdotaccent.alt public.kern2.Eng Eng M N Nacute Ncaron Ncommaaccent Ntilde afii61352 public.kern2.J J Jcircumflex public.kern2.Oslash Oslashacute Oslash public.kern2.S S Sacute Scaron Scedilla Scircumflex Scommaaccent dollar public.kern2.S.alt Sacute.alt Scaron.alt Scedilla.alt Scircumflex.alt Scommaaccent.alt S.alt dollar.alt public.kern2.T T Tbar Tcaron Tcommaaccent uni021A public.kern2.U U Uacute Ubreve Ucircumflex Udieresis Ugrave Uhungarumlaut Umacron Uogonek Uring Utilde public.kern2.W W Wacute Wcircumflex Wdieresis Wgrave public.kern2.Y Y Yacute Ycircumflex Ydieresis public.kern2.Z Z Zacute Zcaron Zdotaccent public.kern2.a a.alt2 aacute.alt2 abreve.alt2 acircumflex.alt2 adieresis.alt2 ae.alt2 aeacute.alt2 agrave.alt2 amacron.alt2 aogonek.alt2 aring.alt2 aringacute.alt2 atilde.alt2 public.kern2.a.alt aacute.alt abreve.alt acircumflex.alt adieresis.alt aeacute.alt agrave.alt amacron.alt aogonek.alt aring.alt aringacute.alt atilde.alt a.alt ae.alt public.kern2.at at at.case public.kern2.b b thorn public.kern2.braceleft braceleft bracketleft public.kern2.braceright braceright bracketright public.kern2.c c cacute ccaron ccedilla ccircumflex cdotaccent d dcaron dcroat e eacute ebreve ecaron ecircumflex edieresis edotaccent egrave emacron eogonek eth o oacute obreve ocircumflex odieresis oe ograve ohungarumlaut omacron oslash oslashacute otilde q a ae c.alt e.alt g.alt cacute.alt ccaron.alt ccedilla.alt ccircumflex.alt cdotaccent.alt eacute.alt ebreve.alt ecaron.alt ecircumflex.alt edieresis.alt edotaccent.alt egrave.alt emacron.alt eogonek.alt gbreve.alt gcircumflex.alt gdotaccent.alt aacute abreve acircumflex adieresis aeacute agrave amacron aogonek aring aringacute atilde gbreve gcircumflex gdotaccent gcommaaccent.alt public.kern2.colon colon semicolon public.kern2.comma comma ellipsis period quotedblbase quotesinglbase public.kern2.copyright copyright copyright.alt registered public.kern2.dotlessi dotlessi i iacute ibreve icircumflex idieresis igrave ij imacron iogonek itilde p eng public.kern2.emdash emdash endash hyphen public.kern2.eng kgreenlandic m n nacute ncaron ncommaaccent ntilde r racute rcaron rcommaaccent public.kern2.f f fi fl public.kern2.g g.alt2 gbreve.alt2 gcircumflex.alt2 gcommaaccent.alt2 gdotaccent.alt2 public.kern2.guillemotleft guillemotleft guilsinglleft public.kern2.guillemotleft.case guillemotleft.case guilsinglleft.case public.kern2.guillemotright guillemotright guilsinglright public.kern2.guillemotright.case guillemotright.case guilsinglright.case public.kern2.h h hbar hcircumflex k kcommaaccent l lacute lcaron lcommaaccent ldot lslash germandbls public.kern2.hyphen.case emdash.case endash.case hyphen.case plus minus public.kern2.j j public.kern2.minute quotedbl quotesingle public.kern2.onesuperior onesuperior threesuperior twosuperior public.kern2.quotedblleft quotedblleft quoteleft public.kern2.quoteright quotedblright quoteright public.kern2.s s sacute scaron scedilla scircumflex scommaaccent public.kern2.s.alt sacute.alt scaron.alt scedilla.alt scircumflex.alt scommaaccent.alt s.alt public.kern2.t t tbar tcaron tcommaaccent uni021B public.kern2.t.alt tcaron.alt tcommaaccent.alt t.alt uni021B.alt public.kern2.t.alt2 tcaron.alt2 tcommaaccent.alt2 t.alt2 uni021B.alt2 public.kern2.u u uacute ubreve ucircumflex udieresis ugrave uhungarumlaut umacron uogonek uring utilde yacute.alt ycircumflex.alt ydieresis.alt y.alt y yacute ycircumflex ydieresis public.kern2.v v w wacute wcircumflex wdieresis wgrave public.kern2.y y.alt2 yacute.alt2 ycircumflex.alt2 ydieresis.alt2 public.kern2.z z zacute zcaron zdotaccent ================================================ FILE: assets/ColdtypeObviously_CompressedBlackItalic.ufo/layercontents.plist ================================================ foreground glyphs ================================================ FILE: assets/ColdtypeObviously_CompressedBlackItalic.ufo/lib.plist ================================================ com.defcon.sortDescriptor ascending .notdef A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z Aacute aacute Abreve abreve Acircumflex acircumflex Adieresis adieresis Agrave agrave Amacron amacron Aogonek aogonek Aring aring Aringacute aringacute Atilde atilde AE ae AEacute aeacute Cacute cacute Ccaron ccaron Ccedilla ccedilla Ccircumflex ccircumflex Cdotaccent cdotaccent Dcaron dcaron Dcroat dcroat Eacute eacute Ebreve ebreve Ecaron ecaron Ecircumflex ecircumflex Edieresis edieresis Edotaccent edotaccent Egrave egrave Emacron emacron Eng eng Eogonek eogonek Eth eth fi fl Gbreve gbreve Gcircumflex gcircumflex Gcommaaccent gcommaaccent Gdotaccent gdotaccent Hbar hbar Hcircumflex hcircumflex Iacute iacute Ibreve ibreve Icircumflex icircumflex Idieresis idieresis Idotaccent Igrave igrave IJ ij Imacron imacron Iogonek iogonek Itilde itilde dotlessi Jcircumflex jcircumflex Kcommaaccent kcommaaccent kgreenlandic Lacute lacute Lcaron lcaron Lcommaaccent lcommaaccent Ldot ldot Lslash lslash Nacute nacute napostrophe Ncaron ncaron Ncommaaccent ncommaaccent Ntilde ntilde Oacute oacute Obreve obreve Ocircumflex ocircumflex Odieresis odieresis Ograve ograve Ohungarumlaut ohungarumlaut Omacron omacron Oslash oslash Oslashacute oslashacute Otilde otilde OE oe Racute racute Rcaron rcaron Rcommaaccent rcommaaccent Sacute sacute Scaron scaron Scedilla scedilla Scircumflex scircumflex Scommaaccent scommaaccent Tbar tbar Tcaron tcaron Tcommaaccent tcommaaccent uni021A uni021B Thorn thorn Uacute uacute Ubreve ubreve Ucircumflex ucircumflex Udieresis udieresis Ugrave ugrave Uhungarumlaut uhungarumlaut Umacron umacron Uogonek uogonek Uring uring Utilde utilde Wacute wacute Wcircumflex wcircumflex Wdieresis wdieresis Wgrave wgrave Yacute yacute Ycircumflex ycircumflex Ydieresis ydieresis Zacute zacute Zcaron zcaron Zdotaccent zdotaccent uni1E9E germandbls C.alt Ccedilla.alt Cacute.alt Ccaron.alt Ccircumflex.alt Cdotaccent.alt G.alt Gbreve.alt Gcircumflex.alt Gcommaaccent.alt Gdotaccent.alt S.alt Sacute.alt Scaron.alt Scedilla.alt Scircumflex.alt Scommaaccent.alt a.alt agrave.alt acircumflex.alt aacute.alt abreve.alt amacron.alt aogonek.alt aring.alt aringacute.alt atilde.alt adieresis.alt ae.alt aeacute.alt c.alt ccedilla.alt cacute.alt ccaron.alt ccircumflex.alt cdotaccent.alt e.alt eacute.alt egrave.alt ecircumflex.alt edieresis.alt ebreve.alt ecaron.alt edotaccent.alt emacron.alt eogonek.alt a.alt2 agrave.alt2 acircumflex.alt2 adieresis.alt2 aacute.alt2 abreve.alt2 amacron.alt2 aogonek.alt2 aring.alt2 aringacute.alt2 atilde.alt2 ae.alt2 aeacute.alt2 g.alt gbreve.alt gcircumflex.alt gdotaccent.alt gcommaaccent.alt g.alt2 gbreve.alt2 gcircumflex.alt2 gdotaccent.alt2 gcommaaccent.alt2 s.alt sacute.alt scaron.alt scedilla.alt scircumflex.alt scommaaccent.alt t.alt tbar.alt tcaron.alt tcommaaccent.alt uni021B.alt t.alt2 tbar.alt2 tcaron.alt2 tcommaaccent.alt2 uni021B.alt2 y.alt yacute.alt ycircumflex.alt ydieresis.alt y.alt2 yacute.alt2 ycircumflex.alt2 ydieresis.alt2 zero one two three four five six seven eight nine two.alt three.alt five.alt six.alt nine.alt period comma colon semicolon ellipsis periodcentered periodcentered.case bullet bullet.case ampersand ampersand.alt exclam exclamdown exclamdown.case question question.alt questiondown questiondown.alt questiondown.case questiondown.alt.case quotedblleft quotedblright quoteleft quoteright quotedblbase quotesinglbase guillemotleft guillemotright guillemotleft.case guillemotright.case guilsinglleft guilsinglright guilsinglleft.case guilsinglright.case hyphen endash emdash hyphen.case endash.case emdash.case underscore slash backslash bar brokenbar parenleft parenright parenleft.case parenright.case bracketleft bracketright bracketleft.case bracketright.case braceleft braceright braceleft.case braceright.case at at.case copyright copyright.alt registered trademark asterisk dagger daggerdbl asciicircum asciitilde dollar dollar.alt cent cent.alt sterling sterling.alt yen Euro Euro.alt numbersign paragraph section section.alt ordfeminine ordfeminine.alt ordfeminine.alt2 ordmasculine degree percent perthousand quotedbl quotesingle plus minus less greater equal multiply divide zero.osf one.osf two.osf three.osf four.osf five.osf six.osf seven.osf eight.osf nine.osf two.osf.alt three.osf.alt five.osf.alt six.osf.alt nine.osf.alt dollar.osf sterling.osf Euro.osf yen.osf dollar.osf.alt sterling.osf.alt Euro.osf.alt acute hungarumlaut caron.alt grave circumflex caron breve tilde macron dieresis dotaccent ring commaaccent cedilla ogonek baraccent afii61352 onesuperior twosuperior threesuperior onequarter onehalf threequarters onehalf.alt threequarters.alt fraction zero.numr one.numr two.numr three.numr four.numr five.numr six.numr seven.numr eight.numr nine.numr two.alt.numr three.alt.numr five.alt.numr six.alt.numr nine.alt.numr zero.dnom one.dnom two.dnom three.dnom four.dnom five.dnom six.dnom seven.dnom eight.dnom nine.dnom two.alt.dnom three.alt.dnom five.alt.dnom six.alt.dnom nine.alt.dnom arrowleft arrowup arrowright arrowdown filledbox space thinspace nbspace Aacute.compact type glyphList com.loicsander.scaleFast guides presets com.typemytype.robofont.background.layerStrokeColor 0.3189906881 0.3189906881 0.3189906881 0.21 com.typemytype.robofont.binarySource /Users/jamesedmondson/Library/Fonts/QuixoPro.otf com.typemytype.robofont.compileSettings.MacRomanFirst com.typemytype.robofont.compileSettings.autohint 0 com.typemytype.robofont.compileSettings.checkOutlines 1 com.typemytype.robofont.compileSettings.createDummyDSIG com.typemytype.robofont.compileSettings.decompose 1 com.typemytype.robofont.compileSettings.generateFormat 0 com.typemytype.robofont.compileSettings.layerName foreground com.typemytype.robofont.compileSettings.path /Users/jamesedmondson/Dropbox/Ohno/Fonts/DevObviously_compressed_black.otf com.typemytype.robofont.compileSettings.releaseMode 0 com.typemytype.robofont.foreground.layerStrokeColor 0.3189906881 0.3189906881 0.3189906881 0.26 com.typemytype.robofont.guideline.magnetic.6lmnulpnLT 5.0 com.typemytype.robofont.guideline.magnetic.MA9AIQ5b3O 5.0 com.typemytype.robofont.guideline.magnetic.agp5ZEBBfz 5.0 com.typemytype.robofont.guideline.magnetic.hGgdg963dV 5.0 com.typemytype.robofont.guideline.magnetic.oeIcVHklWS 5.0 com.typemytype.robofont.guideline.magnetic.tBJk483qt3 5.0 com.typemytype.robofont.guideline.magnetic.uDFof2HFx3 5.0 com.typemytype.robofont.guideline.showMeasurements.MA9AIQ5b3O com.typemytype.robofont.guideline.showMeasurements.agp5ZEBBfz com.typemytype.robofont.guideline.showMeasurements.hGgdg963dV com.typemytype.robofont.guideline.showMeasurements.tBJk483qt3 com.typemytype.robofont.guideline.showMeasurements.uDFof2HFx3 com.typemytype.robofont.italicSlantOffset 0 com.typemytype.robofont.segmentType curve com.typemytype.robofont.shouldAddPointsInSplineConversion 0 com.typesupply.MetricsMachine4.groupColors @MMK_L_A 1.0 0.0 0.0 0.25 com.typesupply.defcon.sortDescriptor ascending .notdef A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z Aacute aacute Abreve abreve Acircumflex acircumflex Adieresis adieresis Agrave agrave Amacron amacron Aogonek aogonek Aring aring Aringacute aringacute Atilde atilde AE ae AEacute aeacute Cacute cacute Ccaron ccaron Ccedilla ccedilla Ccircumflex ccircumflex Cdotaccent cdotaccent Dcaron dcaron Dcroat dcroat Eacute eacute Ebreve ebreve Ecaron ecaron Ecircumflex ecircumflex Edieresis edieresis Edotaccent edotaccent Egrave egrave Emacron emacron Eng eng Eogonek eogonek Eth eth fi fl Gbreve gbreve Gcircumflex gcircumflex Gcommaaccent gcommaaccent Gdotaccent gdotaccent Hbar hbar Hcircumflex hcircumflex Iacute iacute Ibreve ibreve Icircumflex icircumflex Idieresis idieresis Idotaccent Igrave igrave IJ ij Imacron imacron Iogonek iogonek Itilde itilde dotlessi Jcircumflex jcircumflex Kcommaaccent kcommaaccent kgreenlandic Lacute lacute Lcaron lcaron Lcommaaccent lcommaaccent Ldot ldot Lslash lslash Nacute nacute napostrophe Ncaron ncaron Ncommaaccent ncommaaccent Ntilde ntilde Oacute oacute Obreve obreve Ocircumflex ocircumflex Odieresis odieresis Ograve ograve Ohungarumlaut ohungarumlaut Omacron omacron Oslash oslash Oslashacute oslashacute Otilde otilde OE oe Racute racute Rcaron rcaron Rcommaaccent rcommaaccent Sacute sacute Scaron scaron Scedilla scedilla Scircumflex scircumflex Scommaaccent scommaaccent Tbar tbar Tcaron tcaron Tcommaaccent tcommaaccent uni021A uni021B Thorn thorn Uacute uacute Ubreve ubreve Ucircumflex ucircumflex Udieresis udieresis Ugrave ugrave Uhungarumlaut uhungarumlaut Umacron umacron Uogonek uogonek Uring uring Utilde utilde Wacute wacute Wcircumflex wcircumflex Wdieresis wdieresis Wgrave wgrave Yacute yacute Ycircumflex ycircumflex Ydieresis ydieresis Zacute zacute Zcaron zcaron Zdotaccent zdotaccent uni1E9E germandbls zero one two three four five six seven eight nine period comma colon semicolon ellipsis periodcentered periodcentered.case bullet bullet.case ampersand exclam exclamdown question questiondown quotedblleft quotedblright quoteleft quoteright quotedblbase quotesinglbase guillemotleft guillemotright guillemotleft.case guillemotright.case guilsinglleft guilsinglright guilsinglleft.case guilsinglright.case hyphen endash emdash hyphen.case endash.case emdash.case underscore slash backslash bar brokenbar parenleft parenright parenleft.case parenright.case bracketleft bracketright bracketleft.case bracketright.case braceleft braceright braceleft.case braceright.case at at.case copyright registered trademark asterisk dagger daggerdbl asciicircum asciitilde dollar cent sterling yen Euro numbersign paragraph section ordfeminine ordmasculine degree percent perthousand quotedbl quotesingle plus minus less greater equal multiply divide zero.osf one.osf two.osf three.osf four.osf five.osf six.osf seven.osf eight.osf nine.osf dollar.osf sterling.osf Euro.osf yen.osf acute hungarumlaut caron.alt grave circumflex caron breve tilde macron dieresis dotaccent ring commaaccent cedilla ogonek acute.case hungarumlaut.case grave.case circumflex.case caron.case breve.case tilde.case macron.case dieresis.case dotaccent.case ring.case afii61352 onesuperior twosuperior threesuperior onequarter onehalf threequarters fraction zero.numr one.numr two.numr three.numr four.numr five.numr six.numr seven.numr eight.numr nine.numr zero.dnom one.dnom two.dnom three.dnom four.dnom five.dnom six.dnom seven.dnom eight.dnom nine.dnom arrowleft arrowup arrowright arrowdown arrowboth arrowupdn uni2196 uni2197 uni2198 uni2199 filledbox space thinspace nbspace a.alt c.alt s.alt e.alt g.alt y.alt G.alt S.alt C.alt two.alt nine.alt three.alt five.alt six.alt question.alt ampersand.alt t.alt g.alt2 type glyphList public.glyphOrder D E L O P T Y C space ================================================ FILE: assets/ColdtypeObviously_CompressedBlackItalic.ufo/metainfo.plist ================================================ creator com.github.fonttools.ufoLib formatVersion 3 ================================================ FILE: assets/README.md ================================================ fontmake -m assets/ColdtypeObviously.designspace -o variable --output-dir=assets ================================================ FILE: assets/logos.ufo/fontinfo.plist ================================================ ascender 750 capHeight 750 descender -250 guidelines postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV unitsPerEm 1000 xHeight 500 ================================================ FILE: assets/logos.ufo/glyphs/contents.plist ================================================ goodhertz_logo_2019 goodhertz_logo_2019.glif ================================================ FILE: assets/logos.ufo/glyphs/goodhertz_logo_2019.glif ================================================ ================================================ FILE: assets/logos.ufo/glyphs/layerinfo.plist ================================================ color 1,0.75,0,0.7 ================================================ FILE: assets/logos.ufo/glyphs.background/contents.plist ================================================ ================================================ FILE: assets/logos.ufo/glyphs.background/layerinfo.plist ================================================ color 0,0.8,0.2,0.7 ================================================ FILE: assets/logos.ufo/layercontents.plist ================================================ foreground glyphs background glyphs.background ================================================ FILE: assets/logos.ufo/lib.plist ================================================ com.defcon.sortDescriptor ascending Latin-1 type characterSet com.typemytype.robofont.segmentType curve public.glyphOrder goodhertz_logo_2019 ================================================ FILE: assets/logos.ufo/metainfo.plist ================================================ creator com.github.fonttools.ufoLib formatVersion 3 ================================================ FILE: assets/noto.py ================================================ #!/usr/bin/env python from io import BytesIO from urllib.request import urlopen from zipfile import ZipFile zipurl = "https://noto-website-2.storage.googleapis.com/pkgs/Noto-unhinted.zip" print("> Downloading the noto fonts (this may take a while)...") with urlopen(zipurl) as zipresp: with ZipFile(BytesIO(zipresp.read())) as zfile: zfile.extractall("assets/Noto-unhinted") ================================================ FILE: buildenv ================================================ export PYTHONUTF8=1 ================================================ FILE: docs/.gitignore ================================================ _build _static/renders *.pdf ================================================ FILE: docs/tutorials/midi.rst ================================================ MIDI ==== There are two primary uses for MIDI in Coldtype programs, and they're kind of completely unrelated to each other, though both do use the MIDI protocol. The first is the use of MIDI controllers to set variables and trigger actions on running coldtype scripts. The second is the use of MIDI files (usually exported from audio software) to control animations. Using MIDI controllers ---------------------- If you launch coldtype with the ``-mi 1`` or ``--midi-info=1`` flag, you should see a list of MIDI controllers/devices currently available on your system. I always have a `Launch Control XL `_ plugged into my computer, and if you don't already have a MIDI Controller, I highly recommend that one, since it's got faders, which are a lot of fun in combination with real-time dynamic typography, and it's great for mixing in Ableton, which has nothing to do with this. With that in mind, I'll use the Launch Control as an example. So with that device plugged in, when I run ``coldtype -mi 1`` in my coldtype virtualenv, I see the following print out in my terminal window: .. code::txt 0 IAC Driver Bus 1 1 USB Midi 2 Launch Control XL 3 Launch Control XL HUI I'm including the unrelated devices because those (or others) might show up on your computer as well, depending on what you have plugged in. Basically, you just want to find the `exact` name of the device you're looking to use with Coldtype. In this case, that’s ``"Launch Control XL"`` (not the one with `HUI` in the name). Once you know that, create a file at the root of user path (aka ``~``), called ``.coldtype.py`` You can do that with a command line invocation, like this: ``touch ~/.coldtype.py``, and you can open it in a code editor like VS Code with ``code ~/.coldtype.py`` Once you’re there, you’ll want to add some Python code to configure the MIDI device of your choice. Here’s an example: .. code:: python MIDI = { "Launch Control XL": { "note_on": { } } } Now save that file and relaunch coldtype with the same invocation for midi-info, i.e. ``coldtype -mi 1`` Now if you hit some buttons on your midi controller, you should see some identifying information about those buttons, like what number they correspond to. For instance, if I hit the button labelled 1 on my Launch Control (the button at the bottom left), I see this printed out in my terminal: ``Launch Control XL `` The pertinent information there is the note number, 73. Now you can go back to ~/.coldtype.py and assign that button to a Coldtype action, like so: .. code:: python MIDI = { "Launch Control XL": { "note_on": { 73: "render_all" } } } Now if you run an animation (like ``coldtype examples/animations/house.py``), when you hit that same button on the Launch Control, it'll start a sequential render of all the frames in the animation. N.B. The actions defined in the ``"note_on"`` dict are completely optional, but it's a great way to control a Coldtype program without having to rely on app focus on on your computer. That is, if you want to render all the frames of an animation while you have Premiere or VS Code open in the foreground of your application (rather than the coldtype viewer), you can just hit the key that corresponds to midi controller number 73 (for example). **Faders**: Of course, triggering actions isn't the exciting use of a MIDI controller in Coldtype. The exciting thing is hooking up a knob or fader to a variable, meaning you can quickly and easily modify a design otherwise defined completely in code. If you run coldtype again with the ``-mi 1`` flag (``coldtype -mi 1``), and move around a fader or a knob on your controller, you should see print outs like this: ``Launch Control XL `` Again, the only thing we’re interested in is the 77 displayed there, which is the controller id. But this time we're not going to edit the coldtype config, we're going to target this value directly in a script, by constructing a ``Generic`` midi controller object, then querying it based on the number above, 77 in this case. The 0.5 supplied as the second argument is the default value of the slider, which we have to supply, given that MIDI controllers do not have a "memory" of their own, i.e. when the script starts, we have no way of reading current knob/fader positions off of the MIDI device. More on saving state further on down. .. code:: python from coldtype import * from coldtype.midi.controllers import Generic @animation(rstate=1) def use_midi(f, rs): controller = Generic("Launch Control XL", rs.midi, channel=9) fader = controller(77, 0.5) # returns a value between 0 and 1 return (P() .oval(f.a.r.take(fader, "mdx").square()) .f(hsl(0.65))) Now if you run that code, you should see a blue circle on your screen — and if you move the first fader on a Launch Control XL, you should see the circle change size. Reading MIDI files for animations --------------------------------- Tutorial coming soon... (in the meantime, check out the examples in the sidebar, 808 and house both MIDI for animations extensively) ================================================ FILE: docs/tutorials/type_design.rst ================================================ Type Design =========== Live Reload UFOs ---------------- One fun thing that coldtype can do out of the box is load ``.ufo`` and ``.designspace`` files, as well as monitor those files for changes. Here’s an example of creating a somewhat complex text setting using the ``assets/ColdtypeObviously_BlackItalic.ufo`` UFO source, then automatically monitoring the UFO source and refreshing the rendered composition based on any changes saved to the UFO. .. code:: python from coldtype import * ufo_ttf = Font("assets/ColdtypeObviously_BlackItalic.ufo") @renderable((1200, 300), watch=[ufo_ttf], bg=0) def ufo_monitor(r): return (StSt("CDELOPTY", ufo_ttf, 250, tu=-150, r=1) .fssw(Gradient.H(r, hsl(0.05, s=0.75), hsl(0.8, s=0.75)) , 0, 15, 1) .align(r)) .. image:: /_static/renders/type_design_ufo_monitor.png :width: 600 Now if you run the code above (via ``coldtype .py``) and then open up the source UFO in RoboFont or another UFO-capable font editor, any changes you save to the source will update automatically in the preview window, if you leave the process hanging. This works because the font is passed to ``watch=`` keyword argument of the ``@renderable`` decorator. This instructs the renderer to monitor that font’s source UFO for changes. (This also works for standard fonts, i.e. ``.ttf`` and ``.otf`` files.) Also worth noting: because the UFO font is loaded above by compiling it to a ttf (coldtype does this automatically via ``FontGoggles`` when you pass a ``.ufo`` to ``Font``), you can also use a coldtype program as a way to test OT feature code in realtime — simply edit the feature code, hit save, & the coldtype program above with automatically update. If you want to address glyphs in a UFO directly by their glyph names, you can also load a ``defcon.Font`` directly, as an alternate representation of the UFO source. (``DefconFont`` is provided by coldtype as an alternate name for ``defcon.Font``) This method skips any typesetting code and uses the glyphs in the UFO as pens, via the helper method ``glyph`` provided by ``P`` (which records a ``defcon.Glyph`` to the given pen). .. code:: python ufo = DefconFont("assets/ColdtypeObviously_BlackItalic.ufo") @renderable((500, 500), watch=[ufo.path]) def defcon_monitor(r): return (P() .glyph(ufo["C"]) .scale(0.5) .align(r) .f(0)) .. image:: /_static/renders/type_design_defcon_monitor.png :width: 250 :class: add-border `N.B.` Rather than passing the ``ufo`` object directly to the ``watch=`` argument, we’ve passed it’s ``.path`` property — in the first example we passed the ``Font`` object directly, but coldtype knows how to handle that natively. For anything other than a ``Font``, you can pass its filesystem representation, meaning you can monitor any file on your computer. .. code:: python generic_txt = Path("docs/tutorials/scratch.txt") @renderable((800, 200), watch=[generic_txt]) def txt(r): return P( StSt( "> " + generic_txt.read_text() + " <", "assets/RecMono-CasualItalic.ttf", 50) .f(0.25) .align(r)) .. image:: /_static/renders/type_design_txt.png :width: 400 :class: add-border ================================================ FILE: examples/.gitignore ================================================ Adobe Premiere Pro Auto-Save ================================================ FILE: examples/alphabet.py ================================================ from coldtype import * glyphs = "abcdefghijklmnopqrstuvwxyz0123456789!.," @renderable("letter", bg=1, fmt="pdf") def alphabet(r): s = Scaffold(r.inset(40)).numeric_grid(6, 7) return (P().enumerate(glyphs, lambda x: StSt(x.el, Font.JBMono(), 60, wght=1) .align(s[x.i]) .f(0))) ================================================ FILE: examples/animations/808.py ================================================ # 808 animation # 📽 You can view this code as an animation here: https://vimeo.com/479376752 📽 from coldtype import * from coldtype.fx.skia import phototype #from coldtype.warping import warp from fontTools.ufoLib import UFOReader # This is the "logo font" for Coldtype, and it's packaged with the library itself obvs = Font.ColdtypeObviously() # Here the `MidiTimeline` comes into play — this is a class that parses midi files and converts the native time values to more useful frame-based time-values, based on a frame-per-second (`fps`) value. # This is also useful to load before the render function, because this MIDI data doesn’t change once our program is loaded. wav = download("https://coldtype.xyz/examples/media/808.wav", ººsiblingºº("media/808.wav")) midi = MidiTimeline( ººsiblingºº("media/808.mid") , duration=120 , bpm=120 , fps=30) ar = { "KD": [5, 50], "CW": [15, 75], "HT": [10, 10], "RS": [5, 5], "SD": [10, 40], "TM": [5, 10] } # I like to keep things like logos in UFO source files, since they aren’t really typographic, so you don’t need to load them as fonts. Here we load a ufo of logos via `UFOReader.getGlyphSet`, which makes keyed lookups of vectors very easy. logos = UFOReader("assets/logos.ufo").getGlyphSet() # OK, here’s the main render function. To start this off, we'll define variables for `kick` and `cowbell`, since we'll be referencing those when we build the initial text lockup in the next code block. # The `fv` method is a little cryptic, but we’re looking for `(f)rame-(v)alues` for given MIDI notes, the `[36]` and `[47]` respectively. (Drumkits in Ableton Live usually begin on 36, so that’s usually where you’ll find the kick when reading a MIDI file generated by Ableton.) @animation(timeline=midi, audio=wav, bg=0) def drummachine(f): drums = f.t kick = drums.ki(36) cowbell = drums.ki(47) # Now we can build the initial `Style` specification, using the MIDI values for kick and cowbell to control the tracking (`tu`), and the `wdth` of the variable font, via the `.ease()` function available on the object returned from an `.fv` call. # We can also re-kern the T/Y for this particular use, since the text is so big, and when it gets stroked later on, we can avoid having a too-large visual mass in the composition. (Also it’s fun to demonstrate that you can easily re-kern fonts with the `kp=` keyword and a dictionary of glyph-name pairs. (`kp` stands for kern-pairs.)) style = Style(obvs, 390, tu=cowbell.adsr(ar["CW"], r=(-150, 400)), wdth=cowbell.adsr(ar["CW"], r=(1, 0.75)), ro=1, r=1, kp={"T/Y":-25}) # Now with the `style` variable settled, we can construct a multi-line text lockup with the `StSt` class, by passing in a rectangle to hold the lockup, then the text itself, and the style object we created above. # We can also set the leading of the multi-line setting here, by specifying `leading=` as a function of the kick signal. pens = (StSt("COLD\nTYPE", style, leading=kick.adsr(ar["KD"], r=(10, 50))) .align(f.a.r)) # So now we’ve got the lockup, and we move from _building_ the text to _messing_ with it, since we converted from the "text" realm to the "vector" realm with that `.pens()` call above. This is analogous to hitting "Convert to Outlines" in Illustrator (except in this case, if we want to change the text later, it’s easy). # Anyway, the point is now we have a structured representation of the text that we can query and modify, kind of like manipulating a DOM via css or js if you're familiar with web tech. # To start, let’s visualize the snare hits by shearing the line composition & rotating the two letters that correspond to where the snares hit in an 8-count. That's a fancy way of saying, the eight letters here correspond to the 8 eighth-notes in the bar, so we want to modify letters 3 and 7 (aka `P` and `L`), which correspond to the snare hits. # A few notes: # - `index` allows you to modify a nested pen value in-place as a set of contours with a callback function (aka a `lambda`). Basically, the `index` function first "explodes" the glyph into all its component contours (in the P’s case, that’s the outer shape & the counter shape), and then gives you an opportunity to modify just the specified contour (at the "index") before reassembling the contours into the letter (so the counter is still a counter and not just another filled-in shape). That’s how we’re able to keep the counter of the P frozen in place. If we got rid of the `index` call and instead rotated the P glyph itself (ala `pens[1].find_(glyphName="P").rotate(rim.ease()*-270)`), then the counter shape would also rotate. snare = drums.ki(40) se, si = snare.adsr(ar["SD"], find=1) if si == 0: # the first snare hit pens[0].translate(-150*se, 0) pens[1].translate(150*se, 0) pens[0].find_(glyphName="L").rotate(se*-270) else: # the second snare hit pens[0].translate(150*se, 0) pens[1].translate(-150*se, 0) pens[1].find_(glyphName="P").index(0, lambda p: p.rotate(se*270)) # Whew, ok that was a little complicated. Now let’s do something similar with `index` on the P, but this time rotate just the counter shape when the second rimshot hits (we can ignore the first rimshot b/c it hits at the same time as a hi-hat, which we'll visualize in a second). rim = drums.ki(39) re, ri = rim.adsr(ar["RS"], rng=(0, -270), find=1) if ri == 1: (pens[1] .find_(glyphName="P") .index(1, lambda p: p.rotate(re))) # Wouldn’t it be cool if the letters corresponding to the kicks scaled up whenever the kick hit? That’s what’s happening here, along with a more programmer-y idiom, i.e. unpacking a tuple to the `line, glyph`. This is just a way of abbreviating a longer `if-else` statement that would contain redundant code. ke, ki = kick.adsr(ar["KD"], rng=(1, 1.5), find=1) line, glyph = (0, "C") if ki == 0 else (1, "Y") (pens[line] .find_(glyphName=glyph) .scale(ke)) # OK, on to the hi-hats. Here we get the hat signal from the midi, with an even preverb-reverb (that’s the `[10, 10]` bit), because we want to mimic the regular action of a drummer hitting a hi-hat. hat = drums.ki(43) he, hi = hat.adsr(ar["HT"], find=1) # When the first hat hits, let’s move the counter in the O to the right. if hi == 0: (pens[0].find_(glyphName="O") .index(1, lambda p: p.translate(80*he, 0))) # And when the second hat hits, let’s move the counter of the D in the first line, this time translating it down & then rotating it, to make it seem like it’s falling and then bouncing back up from the bottom of the outer shape. elif hi == 1: (pens[0].find_(glyphName="D") .index(1, lambda p: p.translate(-30*he, -100*he).rotate(he*110))) # And when the third and fourth hats hit, let’s move the left and right sides of the T crossbar, via the `indices` function, which lets you adjust the x and y values of a certain set of points (at the given indices) directly via a callback. elif hi == 2: pens[1].find_(glyphName="T").indices(range(0, 7), lambda p: p.o(-150*he, 0)) elif hi == 3: pens[1].find_(glyphName="T").indices(range(26, 35), lambda p: p.o(150*he, 0)) # Ok last hat! Here we exaggerate the horizontality of the E counters with the same `indices` function. elif hi == 4: (pens[1].find_(glyphName="E") .indices(range(10, 15), lambda p: p.o(-35*he, 0)) .indices(range(23, 30), lambda p: p.o(-75*he, 0))) # The last visualization is the tom-tom hit. Here we can just nudge up the outer contour of the O in the first line. tom = (drums.ki(50) .adsr(ar["TM"], rng=(0, -80))) (pens[0].find_(glyphName="O").index(0, lambda p: p.translate(0, tom))) # And a little branding: load the Goodhertz logo from the ufo via `glyph` ghz_logo = (P() .glyph(logos["goodhertz_logo_2019"]) .scale(0.2) .align(f.a.r, y="mny") .translate(0, 100) #.ch(warp(None, speed=f.e("l", 1, rng=(0, 3)), rz=3, mult=50)) .skew(cowbell.adsr(ar["CW"], rng=(0, 0.5)))) # Now we return the data we’ve manipulated to the renderer. This is also where we apply the finishing touches to the `COLD\nTYPE` lockup, by reversing the lines so that the the first line is last (meaning it shows up on top, which is nice for when the C gets really big), and then by applying an `understroke`, which interleaves stroked copies of each letter in the composition, giving us a classic look that we can hit with a high-contrast `phototype` simulation. And then we’re done! return P( ghz_logo.f(hsl(0.9, 0.55, 0.5)), (pens.fssw(1, 0, 15, 1) .reverse() .translate(0, 100) .ch(phototype(f.a.r, cut=190, cutw=8)))) ================================================ FILE: examples/animations/_audio.py ================================================ from coldtype import * from coldtype.timing.audio import Wavfile from coldtype.fx.skia import phototype """ You'll need to `pip install soundfile` in yourt virtualenv to get this to work """ audio = Wavfile("examples/animations/media/coldtype.wav") obvs = Font("assets/ColdtypeObviously.designspace") @animation(timeline=audio.framelength, bg=0, audio=audio.path) def render(f): amp = audio.amp(f.i) return (StSt("COLDTYPE", obvs, 700, tu=-50+150*(0.2+pow(amp,2)*5), rotate=5+10*amp, wdth=0, r=1, ro=1) .align(f.a.r) .fssw(hsl(0.9, s=0.6, l=0.4), 1, 20, 1)) ================================================ FILE: examples/animations/_drumsolo.py ================================================ from coldtype import * from coldtype.fx.skia import phototype audio = __sibling__("media/68.wav") midi = MidiTimeline( __sibling__("media/68.mid") , bpm=151) @animation(timeline=midi, bg=hsl(0.4, 0.8, l=0.2), render_bg=1, audio=audio) def drumsolo(f): d = lambda notes, a, r: f.t.ki(notes).adsr([a, r]) lk1 = { "O": d([36, 38], 5, 50), "M": d([42, 62, 63], 3, 20), "U": d([60, 61, 64], 3, 10), "H": d([81, 93, 94, 98], 3, 10), "R": d([49, 50, 65, 71], 3, 20), "D": d([72, 73, 74], 3, 50), "S": d([52, 54, 86], 3, 50), "P": d([51], 3, 350) } return (Glyphwise("DRUM\nSHOP", lambda g: [ Style(Font.MutatorSans(), 350), dict( wdth=lk1.get(g.c, 0), wght=0.25*lk1.get(g.c, 0) )]) .lead(20) .xalign(f.a.r, tx=0) .align(f.a.r, tx=0) .f(1) .pen() .ch(phototype(f.a.r, blur=2, cut=190, cutw=25, fill=hsl(0.35, 0.8, l=0.75)))) release = drumsolo.export("h264", audio=__sibling__("media/68.wav"), vf="eq=brightness=0.0:saturation=1.5", loops=2) ================================================ FILE: examples/animations/_simple.py ================================================ from coldtype import * @animation(timeline=Timeline(120, 30)) def simple(f): try: x, y = f.rec["cursor"].get(str(f.i)) except: x, y = 0, 0 out = P().enumerate(f.rec["cursor"], lambda x: P().oval(Rect.FromCenter(x.el, 10)).f(0 if x.i == 0 else hsl(x.i*0.01, 1, 0.8))) return P( #(P().rect(f.a.r.inset(f.e("eeio", r=(250, 500)))) # .f(hsl(0.65))), out, (P().oval(Rect.FromCenter((x, y), 100)).fssw(-1, 0, 1)) ) ================================================ FILE: examples/animations/access_frame.py ================================================ from coldtype import * from coldtype.raster import * @animation(Rect(540, 540), bg=1) def numbers(f:Frame): # Do an inline rasterization # which returns a skia.Image, which can do lots # of things # https://github.com/kyamagu/skia-python/blob/main/notebooks/Python-Image-IO.ipynb skia_image = (StSt(str(f.i), Font.JBMono(), 300) .align(f.a.r) .chain(rasterized(f.a.r, wrapped=False))) print(type(skia_image)) # skia.Image # to return it to the standard coldtype renderer, # you’ll need to wrap it with a SkiaImage (from coldtype.raster) return SkiaImage(skia_image) # alternate method — access disk-rendered frames from first animation (only works if you render all the frames of the first animation, by hitting the 'a' key with the viewer open) @animation(Rect(540, 540), bg=1) def numbers_from_rendered(f:Frame): path = numbers.pass_path(f.i) if path.exists(): print(type(path)) # pathlib.Path return SkiaImage(path) ================================================ FILE: examples/animations/adsr.py ================================================ from coldtype import * from coldtype.timing.nle.ascii import AsciiTimeline at = AsciiTimeline(2, 30, """ < [0 ] [1 ] [2 ] [3 ] snare """) @animation((1080, 1080), timeline=at, bg=1, render_bg=1) def adsr(f): # line1 is a "standard" Glyphwise application # where the position of the letters is constantly # changing, since the variations are changing line1 = (Glyphwise("COLD", lambda g: Style(Font.MutatorSans(), 350 , wght=at.ki(f"{g.i}").adsr([5, 10], ["sei", "ceo"]) , wdth=at.ki(f"{g.i}").adsr([5, 50], ["eei", "eeo"]))) .align(f.a.r.take(0.5, "N"), tx=1)) # line2 is non-standard, in that two values are # returned in the Glyphwise lambda: a "base" style # and a dict of modifications, so that the base # style is used to position the letters, and then # the modifications change the letters without # repositioning them line2 = (Glyphwise("TYPE", lambda g: [Style(Font.MutatorSans(), 350), dict( wght=at.ki(f"{g.i}").adsr([5, 30], ["sei", "ceio"], dv=1, rs=1), wdth=at.ki(f"{g.i}").adsr([5, 30], ["eei", "eeio"], dv=0.25, rs=1))]) .align(f.a.r.take(0.5, "S"), tx=0) .removeOverlap()) # now we can put them together # and add a center line return (P(line1, line2) .append(P(f.a.r .take(at .ki("snare") .adsr(rng=(4, 100)), "CY") .inset(-20, 0))) .fssw(-1, 0, 2)) ================================================ FILE: examples/animations/adsr_ascii.py ================================================ from coldtype import * at = AsciiTimeline(1, 30, """ < T T T """) @animation((1080, 1080), timeline=at, bg=0) def choreography(f): v = at.ki("T").adsr([8, 110]) return (StSt("T", Font.MutatorSans(), 500 , wght=v) .align(f.a.r) .f(1)) ================================================ FILE: examples/animations/alphabet.py ================================================ from coldtype import * tl = Timeline(26, fps=18) @animation(rect=(1080, 1080), timeline=tl, bg=0) def render(f): pe = f.e(e:="qeio", 1) return (P( StSt(chr(65+f.i), Font.MutatorSans() , f.e(e, r=(500, 750)) , wdth=1-pe , wght=pe) .fssw(-1, hsl(pe, s=0.6, l=0.6), 3) .align(f.a.r, ty=1) .translate(0, 50) .removeOverlap(), StSt("{:02d}".format(f.i), Font.RecursiveMono(), 24, wdth=1) .pen() .align(f.a.r.take(150, "mny"), tx=0) .f(hsl((1-pe)+0.5)))) ================================================ FILE: examples/animations/alternate_glyphs.py ================================================ from coldtype import * at = AsciiTimeline(3, 30, """ < [ss06 ] [ss07 ] """) @animation((1400, 540), timeline=at, bg=1, render_bg=1) def scratch(f): return (StSt("alter-\nnates", "SFCompactItalic", 200 , ss06=at.ki("ss06").on() , ss07=at.ki("ss07").on()) .align(f.a.r)) ================================================ FILE: examples/animations/ascii_choreography.py ================================================ from coldtype import * from coldtype.timing.nle.ascii import AsciiTimeline at = AsciiTimeline(3, 30, """ < [0 ][1 ][2 ][3 ] [0w ] [2w ] [1w ] [3w ] [ro ] [tu ] """) @animation((1080, 1080), timeline=at, bg=hsl(0.9, 1, 0.9)) def choreography(f): letter = at.ki(list("0123")) li, lidx = letter.e(find=1) return (Glyphwise("TYPE", lambda g: Style(Font.MutatorSans(), 200, wght=at.ki(g.i).e(), wdth=at.ki(f"{g.i}w").e("qeio", 1))) .track(at.ki("tu", f.i).e(r=150)) .align(f.a.r) .f(hsl(0.7, 1)) .mapv(lambda i, p: p .rotate(at.ki("ro", f.i-i).e("eeio", 0, r=-360))) .centerPoint(f.a.r, (lidx, "tyC"), i=li) .scale(letter.e(r=(1,4)), pt=(lidx, "tyC"))) ================================================ FILE: examples/animations/ascii_keyframe_positions.py ================================================ from coldtype import * at = AsciiTimeline(2, """ < A A B B C C D D E F G H """) r = Rect(1000) s = Scaffold(r).numeric_grid(4) rs = dict( keyframes={ "A": dict(r=s["0|3"].r, ro=0, wght=0), "B": dict(r=s["2|0"].r, ro=0, wght=1), "C": dict(r=s["0|1"].r, ro=90, wght=0.5), "D": dict(r=s["3|2"].r, ro=0, wght=1)}) txt = dict( keyframes={ "E": dict(txt="A"), "F": dict(txt="B"), "G": dict(txt="C"), "H": dict(txt="D")}) @animation(r, tl=at, bg=1) def scratch(f): kfs1 = f.t.kf(**rs) kfs2 = f.t.kf(**txt) return (StSt(kfs2["txt"], Font.MuSan(), 100 , wght=kfs1["wght"]) .f(1) .align(kfs1["r"]) .insert(0, P(kfs1["r"].inset(10)).f(0)) .rotate(kfs1["ro"])) + s.borders() ================================================ FILE: examples/animations/ascii_keyframes.py ================================================ from coldtype import * from coldtype.css import * at = AsciiTimeline(1, """ < ~i ~o A A B B C C D D ~x ~y """, keyframes={ "A": dict(fontSize=100), "B": dict(fontSize=250), }, eases={ "i": cubicBezier(0.25,0.1,0.25,3.5), "o": "qeio" }) @animation(tl=at, bg=0) def css1(f): return [ (StSt("OK", Font.MutatorSans(), **{ **f.t.kf(), **f.t.kf( keyframes=dict(C=dict(wght=0), D=dict(wght=1)), eases=dict(x="eeo", y="eeo")), **f.t.kf( keyframes=dict(A=dict(wdth=1), B=dict(wdth=0)), eases=dict(i="sei", o="eei"))}) .align(f.a.r) .f(1))] ================================================ FILE: examples/animations/ascii_keyframes2.py ================================================ from coldtype import * from coldtype.css import * r = Rect(1080) at = AsciiTimeline(1, """ < ~i ~o A A B B C C D D ~x ~y """) r1s = dict( keyframes={ "A": dict(r1=r.take(200, "SW")), "B": dict(r1=r.take(300, "NE"))}, eases={"i": "eeo", "o": "eei"}) r2s = dict( keyframes={ "C": dict(r2=r.take(300, "NW"), f=hsl(0)), "D": dict(r2=r.take(200, "SE"), f=hsl(0.1))}, eases={"x": "seio", "y": "seio"}) @animation(r, tl=at, bg=0) def kf(f): r1 = f.t.kf(**r1s) r2 = f.t.kf(**r2s) return P(r1["r1"]).f(1) + P(r2["r2"]).f(r2["f"]) ================================================ FILE: examples/animations/ascii_keyframes_entrance.py ================================================ from coldtype import * at = AsciiTimeline(2, """ < N W W A B B A ~i ~o C D D E """) @animation((1080, 540), tl=at, bg=0, release=lambda x: x.gifski()) def keyframes(f): def variate(x): fi = f.i - x.i wdth = at.kf(fi=fi , keyframes=dict(N=0, W=1) , eases=dict(i="eio", o="eei")) return Style(Font.ColdObvi(), 100, wdth=wdth) def animate(i, p): fi = f.i - i*1 ty = at.kf(fi=fi , keyframes=dict(A=-360, B=0) , eases=dict(i="eeo", o="eeio")) r = at.kf(fi=fi , keyframes=dict(C=360, D=0, E=-360*2) , eases=dict(i="eeio", o="seio")) p.t(0, ty if i%2 != 0 else -ty).rotate(r) return (Glyphwise("COLDTYPE", variate) .align(f.a.r) .mapv(animate) .reverse() .fssw(1, 0, 9, 1)) ================================================ FILE: examples/animations/ascii_pixels.py ================================================ from coldtype import * from coldtype.fx.skia import precompose at = AsciiTimeline(1, 18, """ < 0 1 2 3 4 """) @animation((1080, 1080/2), timeline=at, bg=hsl(0.6, 1, 0.7)) def pixellation(f): return (StSt("PIXEL", "Times New Roman", 460, tu=250) .f(1) .scale(0.5, 1) .align(f.a.r) .map(lambda i, p: p .ch(precompose(f.a.r , scale=at.ki(i).adsr([5, 25], ["qeio", "l"], r=(0.25, 0.015)))))) ================================================ FILE: examples/animations/ascii_simple.py ================================================ from coldtype import * from coldtype.timing.nle.ascii import AsciiTimeline at = AsciiTimeline(4, """ < [1 ] [1 ] [0 ] [2 ] [wght ] """) @animation((1080, 540), timeline=at) def ascii(f): return (Glyphwise("AAA", lambda g: Style(Font.MutatorSans(), 120, wdth=at.ki(g.i).io(8), wght=at.ki("wght").io(10) )) .align(f.a.r, ty=1) .mapv(lambda i, p: p .scale(at.ki(i).io(8, r=(1, 1.35))))) ================================================ FILE: examples/animations/ascii_twostep.py ================================================ from coldtype import * from coldtype.timing.nle.ascii import AsciiTimeline at = AsciiTimeline(1, """ < [s ] [a ] [b ] [c ] """) @animation((1080, 540), timeline=at, bg=1, render_bg=1) def twostep(f): return (Glyphwise("COLD\nTYPE", lambda g: Style(Font.ColdtypeObviously(), 100, wdth=(at.ki("a" if g.l < 1 else "b").io(10)), tu=at.ki("c").io(8, r=(0, 500)))) .lead(10) .xalign(f.a.r) .align(f.a.r) .scale(at.ki("s").io(20)*2) .f(0) .layer(1, lambda p: P() .rect(p.ambit(tx=1, ty=1).inset(-50)) .fssw(-1, 0, 2) .___null())) ================================================ FILE: examples/animations/ascii_words.py ================================================ from coldtype import * at = AsciiTimeline(1, 30, """ < [.big ] *Oh, hello. • *This ≈some ≈t +e +x +t +. • is timed """) def styler(c): if "big" in c.styles: return c.text.upper(), Style(Font.MutatorSans(), 100, wght=1) else: return c.text, Style(Font.RecursiveMono(), 100) @animation((1080, 540), tl=at, offset=0) def timedWords(f): return (f.t.words.currentGroup() .pens(f, styler) #.printh() .cond(f.t.words.styles.ki("big").on(), lambda p: p.f(hsl(0.9))) .lead(30) .xalign(f.a.r) .align(f.a.r) .removeFutures()) # RENDERABLES = [ # timedWords.viewOffset(30) # ] ================================================ FILE: examples/animations/avoidance.py ================================================ from coldtype import * import numpy as np # after https://editor.p5js.org/creativecoding/sketches/ncNWaEkTw minMouseDist = 20000 words = """ Just getting out of the way """ # https://stackoverflow.com/questions/21030391/how-to-normalize-a-numpy-array-to-a-unit-vector def normalized(a, axis=-1, order=2): l2 = np.atleast_1d(np.linalg.norm(a, order, axis)) l2[l2==0] = 1 return a / np.expand_dims(l2, axis) def displacer(r, c): txt = (StSt(words, "Times", 100) .lead(20) .align(r) .collapse()) for g in txt: p = g.ambit(tx=1, ty=1).psw mouseDist = Line(p, c).length() p2 = np.array([p.x - c.x, p.y - c.y]) distDifference = minMouseDist - mouseDist p2x, p2y = (normalized(p2) * math.sqrt(distDifference))[0] g.t(p2x, p2y) return P( P().oval(Rect.FromCenter(c, 50)).f(1), txt.f(1)) #@ui(bg=0) def avoid_ui(u): return displacer(u.r, u.c) @animation(tl=Timeline(500, 30), bg=0, render_bg=1) def avoid_anim(f): o = P().oval(f.a.r.take(f.e("eeio", 7, r=(900, 100)), "CX").square()) c, _ = o.point_t(f.e("seio", 3)) return displacer(f.a.r, c) ================================================ FILE: examples/animations/banner.py ================================================ from coldtype import * from coldtype.fx.skia import phototype kfs = [ dict(wdth=0, rotate=0, tu=300), dict(wdth=1, rotate=15, tu=-150), dict(wdth=0.25, rotate=-15, tu=350), dict(wdth=0.75, rotate=0, tu=-175), dict(wdth=0.5, rotate=25, tu=100), ] # Demonstrating slight ambiguity # in single-frame vs. block syntax # i.e. equivalent when multiplier=1 # but different at multiplier>1 at = AsciiTimeline(3, 30, """ < [0 ] [1 ] [2 ] [3 ] [4 ] A A B B C C D D E E """, { "0": kfs[0], "1": kfs[1], "2": kfs[2], "3": kfs[3], "4": kfs[4], "A": kfs[0], "B": kfs[1], "C": kfs[2], "D": kfs[3], "E": kfs[4], }) @animation(timeline=at, bg=1, rect=(1500, 300*2)) def render(f): return P( StSt("COLDTYPE", Font.ColdObvi(), 250 , **at.kf("eeo", lines=(1,)) , r=1 , ro=1) .align(f.a.r.take(0.5, "N"), tx=0) .fssw(1, 0, 20) .sf(1) .ch(phototype(f.a.r, 1.5, 200, 30, 0)), StSt("COLDTYPE", Font.ColdObvi(), 250 , **at.kf("eeo", lines=(2,)) , r=1 , ro=1) .align(f.a.r.take(0.5, "S"), tx=0) .fssw(1, 0, 20) .sf(1) .ch(phototype(f.a.r, 1.5, 200, 30, 0))) ================================================ FILE: examples/animations/bitmap_font.py ================================================ from coldtype import * from coldtype.runon import RunonEnumerable from coldtype.drawbot import tobp # mac-only # inspired by https://gist.github.com/connordavenport/12cc057798b175782431a883e63386db @animation((1080, 540), bg=0, tl=60) def bitmap_font(f): txt = (StSt("COLD\nTYPE", Font.ColdtypeObviously(), 250 , wdth=f.e("eeio") , tu=f.e("eeio", rng=(50, 350)) , leading=30) .align(f.a.r) .pen() .chain(tobp)) def enumerator(x:RunonEnumerable): if txt.pointInside(x.el.r.pc): return P().rect(x.el.r.inset(1)) return P().enumerate(f.a.r.grid(50, 25), enumerator).f(1) ================================================ FILE: examples/animations/blendmode.py ================================================ from coldtype import * from coldtype.fx.skia import phototype @animation(bg=0, timeline=Timeline(30, 15), render_bg=1) def plus(f): def lockup(adj, fill): return (Glyphwise("ABC", lambda g: Style(Font.MutatorSans(), 750, wght=f.adj(adj-g.i*5).e("eeio", 1), wdth=(fa:=f.adj(adj-5)).e("eeio", 1, rng=(1, 0)), tu=fa.e("eeio", 1, rng=(-250, 0)), ro=1)) .mapv(lambda p: p.align(f.a.r, ty=1, tx=1)) .pen() .fssw(-1, 1, 10) .ch(phototype(f.a.r, blur=5, cut=f.e("seio", 2, rng=(50, 150)), cutw=15, fill=fill)) .blendmode(BlendMode.Plus)) return P( lockup(0, hsl(f.e("l"))), lockup(1.25, hsl(f.adj(-5).e("l"))), lockup(3.5, hsl(f.adj(-20).e("l")))) release = plus.export("h264", loops=6) ================================================ FILE: examples/animations/bounce.py ================================================ from coldtype import * @animation(tl=60, bg=1) def scratch(f): ri = f.a.r.inset(50) a = (StSt("COLD", Font.ColdObvi(), 130, wght=1, wdth=1, fit=ri.w-10, tu=500, tl=500)).scaleToWidth(ri.w).align(ri, "S").skew(-1, 0) b = (StSt("COLD", Font.ColdObvi(), 470, wght=1, wdth=0.5, fit=ri.w-10)).scaleToWidth(ri.w).align(ri, "N") def interp(x): e = f.adj(x.i*-2).e("eeo", 1) return (x.el.copy() .interpolate(e, b[x.i].copy())) return P().enumerate(a, interp).f(0) ================================================ FILE: examples/animations/colrv1_foldit.py ================================================ from coldtype import * at = AsciiTimeline(2, 30, """ [fold ] < [it ] """) @animation((1080, 540/2), timeline=at, bg=0) def test1(f): font = Font.Find("Foldit\[wght\]") text = "Foldit" br = (Glyphwise(text, lambda g: Style(font , fontSize=200 , wght=at.ki("it").io(10, ["eei", "eeo"]) if g.i > 3 else at.ki("fold").io(10))) .align(f.a.r, tx=0, ty=0)) return P( br, #br.substructure() ) release = test1.export("h264", loops=3) ================================================ FILE: examples/animations/colrv1_nabla.py ================================================ from coldtype import * @animation((1080, 540), tl=30, bg=hsl(0.7, 0.4, 0.4)) def nabla1(f): return (Glyphwise("COLRv1", lambda x: Style("Nabla-Regular-VariableFont_EDPT,EHLT", 300 , EDPT=f.adj(x.i*20).e("eei", r=(0.1, 1)))) .align(f.a.r, tx=0, ty=0) .translate(0, -30)) ================================================ FILE: examples/animations/countdown.py ================================================ from coldtype import * from coldtype.raster import * from coldtype.fx.motion import filmjitter at = AsciiTimeline(3, 18, """ < C O L D T Y P E """).inflate() rs = random_series(seed=8) @animation((1080, 800) , tl=at , bg=0 , composites=1 , render_bg=1 , release=lambda x: x.export("h264", loops=4)) def countdown(f:Frame): c = at.current() def postprocess(result): return P( P(f.a.r).f(hsl(0.7, 0.7, 0.8)), result.ch(luma(f.a.r, hsl(0.7, 0.7, 0.4)))) return (P(f.a.r) .ch(spackle(xs=0.35, ys=0.35, cut=230, fill=bw(0), base=f.i)) .ups() .insert(0, P(f.a.r).f(1, 0.1)) .insert(0, f.last_render( filmjitter(f.e("l"), speed=(50, 50), scale=(3, 5)))) .append(StSt(c.name, Font.MutatorSans() , fontSize=c.e("sei", 0, rng=(100, 1200)) , wdth=c.e("sei", 0) , wght=rs[c.idx+1]*0.25 , ro=1) .fssw(1, 1, 13) .align(f.a.r, tx=1, ty=1) .blendmode(BlendMode.Xor)) .ch(phototype(f.a.r , cut=139, blur=2.5, cutw=20, fill=1)) .postprocess(postprocess)) ================================================ FILE: examples/animations/custom_ease.py ================================================ from coldtype import * from coldtype.fx.xray import skeleton @animation(timeline=60, bg=1) def easer(f): ease_curve = P().withRect(1000, lambda r, p: p .moveTo(r.psw) .ioEaseCurveTo(r.pn, 2, 50) .ioEaseCurveTo(r.pse, 21, 30)) return P( ease_curve.copy() .scaleToWidth(f.a.r.w-50) .align(f.a.r) .layer( lambda p: p.fssw(-1, hsl(0.9), 2), lambda p: p.ch(skeleton(scale=1.25)).s(hsl(0.65))), StSt("COLD", Font.ColdtypeObviously() , 300 , wdth=f.e(ease_curve, 0)) .align(f.a.r) .fssw(-1, (0, 0.5), 2)) ================================================ FILE: examples/animations/custom_output.py ================================================ from coldtype import * class custom_output_animation(animation): def pass_path(self, index=0): return Path("custom_output_folder") / self.name / f"{self.pass_prefix()}{self.pass_suffix(index)}.{self.fmt}" @custom_output_animation(bg=0) def wght(f): return (StSt("CUSTOM\nOUTPUT", Font.MuSan(), 200, wght=f.e()) .align(f.a.r) .f(1)) ================================================ FILE: examples/animations/delay.py ================================================ from coldtype import * from coldtype.fx.skia import color_phototype @animation((1500, 800), timeline=Timeline(60, fps=23.976)) def recur(f, depth=0): cold = (StSt("COLDTYPE", Font.ColdObvi() , fontSize=f.e("qeio", r=(800, 100)) , wdth=f.e("qeio") , tu=f.e("qeio", r=(-90, -40)) , ro=1 , r=1) .align(f.a.r) .fssw(1, 0, 23-depth) .ch(color_phototype(f.a.r , blur=2+depth*5 , cut=120+depth*5 , rgba=[1, 0, 1, 1])) .ups() .blendmode(BlendMode.Luminosity)) if depth < 5: cold.insert(0, recur.func(Frame((f.i-4)%recur.duration, f.a) , depth=depth+1)) if depth == 0: return cold.ch(color_phototype(f.a.r, blur=3)) else: return cold ================================================ FILE: examples/animations/drumsolo2.py ================================================ from coldtype import * audio = __sibling__("media/c78.wav") midi = MidiTimeline(__sibling__("media/c78.mid") , bpm=120 , lookup={ 0: (36, 41), # kicks 1: 37, # snare 2: 45, # tom-lo 3: 47, # tom-hi }) @animation((1080, 540), timeline=midi, bg=0, render_bg=1) def drumloop(f): kicks = f.a.t.ki(0, f.i).index() def styler(g): # return an array # — first is style for metrics # - second is mods to style for animation return [Style(Font.MuSan(), 350, tu=100), dict(wdth=f.a.t.ki(g.i).adsr((10, 130)), wght=f.a.t.ki(g.i).adsr((5, 20), r=(0, 1)))] s = hsl(0.3) if kicks == 0 else hsl(0.8) return (Glyphwise("DRUM", styler) .align(f.a.r, tx=0) .fssw(-1, s, 4) .sm(0.5)) ================================================ FILE: examples/animations/dswatch.py ================================================ from coldtype import * from coldtype.fx.skia import phototype, shake """ Saving a change to UFOs included in the designspace file should automatically update the coldtype window """ ds = Font("assets/ColdtypeObviously.designspace") @animation(timeline=(120, 23.976), watch=[ds], bg=0) def dswatch(f): return (StSt("COLD", ds, 510 , wdth=f.e(1) , tu=250+-450*(f.e(1)) , r=1 , ro=1) .align(f.a.r) .fssw(0, 1, 5) .chain(shake(5, 2, seed=f.i)) .chain(phototype(f.a.r, blur=3, cut=155, cutw=20, fill=1))) ================================================ FILE: examples/animations/dvd.py ================================================ from coldtype import * font, fontSize, txt = [ [Font.MutatorSans(), 100, "ABC"], ["MDNichrome-V", 100, "DVD"], ["OhnoFatfaceV", 180, "Aa"], ["PeshkaV", 100, "vari"], ["PlakatoDraw", 150, "DVD"] ][0] border = Rect(500, 250) speed = 5 initial = dict(x=250, y=250, c=0, xs=speed, ys=speed) @animation(tl=Timeline(300, 48), memory=initial, bg=0) def scratch(f, m): r = f.a.r.inset(10) m.x += m.xs m.y += m.ys if m.x <= r.x or (r.x + m.x + border.w) >= f.a.r.mxx: m.xs = -m.xs m.c += random()*0.35 if m.y <= r.y or (r.y + m.y + border.h >= f.a.r.mxy): m.ys = -m.ys m.c += random()*0.35 a, b = (r.drop(border.w, "E") .drop(border.h, "N") .ipos(Point(m.x, m.y))) c = hsl(m.c, 1, 0.8) return P( P().oval(border).t(m.x, m.y).fssw(-1, c, 10), (StSt(txt, font, fontSize , fvar_1=a , fvar_0=b ) .f(c) .align(border.offset(m.x, m.y), tx=0))) ================================================ FILE: examples/animations/ec.py ================================================ from coldtype import * at = AsciiTimeline(2, """ < [A ] [A ] [A ] [A ] [B ] """) @animation((540, 540), tl=at) def scratch(f): return (StSt("I", Font.MuSan(), 300 , wdth=f.t.ki("B").e("eeio") ) .align(f.a.r, ty=0) .unframe() .rotate(f.t.ki("A").ec("eeio", rng=(0, 90))) .f(0)) ================================================ FILE: examples/animations/flyin.py ================================================ from coldtype import * at = AsciiTimeline(2, 30, """ < a a b b """) @animation((1080, 1080), tl=at, bg=1) def scratch(f): a = (StSt(txt:="TEXT" , font:=Font.MuSan() , fs:=200 , wght=0 , wdth=1) .align(ri:=f.a.r.inset(110), "N")) b = (StSt(txt, font, fs-20, wght=1, wdth=1).align(ri, "S")) def interp(x): e = at.kf(keyframes=dict(a=0, b=1), offset=x.e*4) return (x.el.copy() .interpolate(e, b[x.i].copy()) .f(hsl(e, 0.9))) return P(a, b, P().enumerate(a, interp)) ================================================ FILE: examples/animations/glyphwise.py ================================================ from coldtype import * from coldtype.fx.skia import phototype fnt = Font.MutatorSans() @animation(timeline=Timeline(90, 24), bg=0, render_bg=1) def glyphwise(f): def styler(g): return [ Style(fnt, 250, wdth=0.15, tu=f.e("seio", rng=(-100, 300))), # metrics Style(fnt, 250 # animated , wdth=f.adj(-g.i*2).e("eeio", 2) , wght=f.adj(-g.i*3).e("seio", 3) , ro=1 , ty=1 )] # should also work w/o an \n in between COLD & TYPE return (Glyphwise("COLD\nTYPE", styler, multiline=1) .xalign(f.a.r, tx=0) .lead(30) .align(f.a.r, tx=0) .reverse(recursive=True) .fssw(1, 0, 7, 1) .ch(phototype(f.a.r, 3, 190, cutw=15))) ================================================ FILE: examples/animations/glyphwise2_rtl.py ================================================ from coldtype import * font_zip_url = "https://github.com/aminabedi68/Estedad/releases/download/7.3/Estedad-v7.3.zip" estedad_variable = Font.UnzipURL(font_zip_url, "Estedad-FD[KSHD,wght].ttf", "Variable") @animation((1080, 540), bg=1, tl=30, release=λ.export("h264", loops=4)) def kashida(f:Frame): txt = "مرحبًا" rh = random_series(seed=1) fs = 200 return (Glyphwise2(txt, lambda g: Style(estedad_variable, fs, wght=f.e("seio"), KSHD=f.adj(-g.i*10).e("seio"))) .mapv(lambda i, p: p.f(hsl(rh[i]))) .align(f.a.r, ty=1)) ================================================ FILE: examples/animations/glyphwise_keyframes.py ================================================ from coldtype import * from coldtype.timing.nle.ascii import AsciiTimeline states = { "A": dict(wdth=0, wght=1, rotate=360), "B": dict(wdth=1, wght=0, rotate=0), "C": dict(wdth=1, wght=1, rotate=0), "D": dict(wdth=0, wght=0, rotate=180), } at = AsciiTimeline(2, 30, """ < [A ][B ][C ][D ] """, states).shift("end", -8) @animation((1080, 520), timeline=at, bg=0) def cheee_wild(f): return (Glyphwise("COLD", lambda g: [ Style(Font.MuSan(), 270, tu=50, ty=1), at.kf("eeio", f.i-g.i*10) ]) .fssw(1, 0, 8, 1) .align(f.a.r, tx=0) .reverse()) ================================================ FILE: examples/animations/glyphwise_wave.py ================================================ from coldtype import * DEBUG = 0 @animation((1080, 1080/2), timeline=30) def cilati_wave(f:Frame): def styler(g:GlyphwiseGlyph): fa = f.adj(g.i*20) return (Style(Font.MuSan(), 270, show_frames=DEBUG, no_shapes=DEBUG, wdth=fa.e(ease:="qeio", 1), wght=fa.e(ease, 1))) return (Glyphwise("COLD\nTYPE".upper(), styler) .xalign(f.a.r) .lead(50) .f(0) .align(f.a.r, tx=0, ty=1)) ================================================ FILE: examples/animations/glyphwise_wave2.py ================================================ from coldtype import * @animation((1080, 1080/2), timeline=50) def fatface_wave(f): return (Glyphwise("COLD", lambda g: (Style(Font.MuSan(), 100+(fa:=f.adj(g.i*4)).e(ease:="seio", 1)*150, wdth=fa.e(ease, 1), wght=fa.e(ease, 1), rotate=-15+30*fa.e(ease, 1)))) .align(f.a.r, h=200) .f(0)) ================================================ FILE: examples/animations/house.py ================================================ from coldtype import * audio = ººsiblingºº("media/house.wav") midi = MidiTimeline( ººsiblingºº("media/house.mid") , bpm=120 , duration=60) @animation(timeline=midi, bg=0.1, audio=audio, release=λ.export("h264", loops=4)) def render(f): k = f.t.ki(36).adsr([12, 10]) s = f.t.ki(38).adsr([4, 35]) c = f.t.ki(48).adsr([0, 50]) hat = f.t.ki(42).index() hues = (0.6, 0.05) if hat < 2 else (0.75, 0.9) style = dict(font=Font.ColdObvi(), fontSize=500, tu=-150, ro=1) return (P( StSt("COLD", style, wdth=1-s*0.5) .f(hsl(hues[0], 0.75, 0.5)) .index(1, lambda p: p.rt((hat+1)*-45)), StSt("TYPE", style, tu=-150-100*k, rotate=-8*k) .f(hsl(hues[1], 0.75, 0.5)) .index([2, 1], lambda p: p.t(70*c, 0))) .xalign(f.a.r) .stack(30) .align(f.a.r.inset(0, 150)) .map(lambda p: p.rp().ssw(0, 10).sf(1)) .rotate(5)) ================================================ FILE: examples/animations/interpolate_roughen.py ================================================ from coldtype import * from coldtype.fx.skia import phototype r = Rect(1080, 1080) a = P().oval(r.inset(200)).flatten(10).roughen(10, seed=3) b = P().oval(r.inset(200)).flatten(10).roughen(2000, seed=1) @animation(r, timeline=90, bg=hsl(0.7)) def cloud(f): return (a.copy().interpolate(f.e("eeio", 1), b) .f(1) .smooth() .ch(phototype(f.a.r, blur=10, cut=210, cutw=3))) release = cloud.export("gif") ================================================ FILE: examples/animations/ipa_vowels.py ================================================ from coldtype import * from coldtype.raster import * from itertools import chain media = ººsiblingºº("media/vowels") mt = MidiTimeline(media / "midi2.mid") audio = media / "midi2.wav" # https://en.wikipedia.org/wiki/IPA_vowel_chart_with_audio # https://brill.com/page/BrillFontDownloads/Download-The-Brill-Typeface font = Font.Find("Brill-Bold") vowels = [ [["i", "y"], ["ɨ", "ʉ"], ["ɯ", "u"]], [["ɪ", "ʏ"], ["ʊ"]], [["e", "ø"], ["ɘ", "ɵ"], ["ɤ", "o"]], [["e̞", "ø̞"], ["ə"], ["ɤ̞", "o̞"]], [["ɛ", "œ"], ["ɜ", "ɞ"], ["ʌ", "ɔ"]], [["æ"], ["ɐ"]], [["a", "ɶ"], ["ä"], ["ɑ", "ɒ"]], ] flat_vowels = list(chain.from_iterable(chain.from_iterable(vowels))) """ not used, but a helpful snippet for processing .ogg's and .opus'es """ def process_wikipedia_audio(): from pydub import AudioSegment for idx, vowel in enumerate(flat_vowels): file = Path(f"~/Downloads/_vowels/{vowel}.ogg").expanduser() if not file.exists(): file = Path(f"~/Downloads/_vowels/{vowel}.opus").expanduser() output = ººsiblingºº(f"media/vowels/{idx}_{vowel}.wav") output.parent.mkdir(exist_ok=True) AudioSegment.from_file(file).export(output, format="wav") #process_wikipedia_audio() r = Rect(1080).inset(100) close = P().m(r.pnw).l(r.pne).ep() open = P().m(open_front:=r.drop(0.55, "W").psw).l(r.pse).ep() front = P().m(r.pnw).l(open_front).ep() back = P().m(r.pne).l(r.pse).ep() central = P().m(r.pn).l(open.point_t(0.5)[0]).ep() def xbar(t): return P().m(front.point_t(t)[0]).l(back.point_t(t)[0]).ep() near_close = xbar(0.165) close_mid = xbar(0.33) mid = xbar(0.5) open_mid = xbar(0.66) near_open = xbar(0.66+0.165) def v(c, line, t): return (StSt("".join(c), font, 70) .f(hsl(0.17, 0.80, 0.75)) .partition(lambda p: p.data("glyphCluster")) .track(20) .align(Rect.FromCenter(line.point_t(t)[0], 1), tx=1) .t(0, 7) .layer( lambda p: P().rect(p.ambit(tx=1, ty=1).inset(-20)).f(0.9).tag("knockout"), lambda p: p.tag("symbols") .map(lambda i, p: p.tag("symbol").data(symbol=c[i])))) def vs(cs, line, ts=(0, 0.5, 1.0)): return P().enumerate(cs, lambda x: v(x.el, line, ts[x.i])) res = (P( vs(vowels[0], close), vs(vowels[1], near_close, [0.15, 0.85]), vs(vowels[2], close_mid), vs(vowels[3], mid), vs(vowels[4], open_mid), vs(vowels[5], near_open), vs(vowels[6], open))) lines = (P(close, open, front, back, central, close_mid, open_mid) .outline(1) .difference(P(res.copy().find("knockout")))) @animation(bg=0, tl=mt, audio=audio, release=λ.export("h264", loops=2)) def chart(f:Frame): r = f.a.r.inset(100) highlight = P() active_symbols = [] current = 0 for idx, symbol in enumerate(res.find("symbol")): if mt.ki(idx+36).on(): highlight.append(P().oval(symbol.ambit(tx=1, ty=0).square(outside=True).inset(-20).offset(0, -5))) active_symbols.append(symbol.data("symbol")) current = idx return (P( lines.f(hsl(0.75, 0.90, 0.58)) .chain(filmjitter(f.e("l", 4, cyclic=0), 1, scale=(1, 1))), highlight.fssw(-1, hsl(0.75, 1.00, 0.65, 0.8), 10), P(res.find("symbols")) .copy() .chain(filmjitter(f.e("l", 4, cyclic=0), 2, scale=(1, 1))), StSt("IPA\nvowel\nchart\nremix", font, 50) .align(f.a.r.inset(76, 90), "SW") .f(hsl(0.17, 0.80, 0.75)) .skew(current/len(flat_vowels)/2, 0), StSt("".join(active_symbols[:1]), font, 920) .align(r.drop(0, "E"), "C", ty=0) #.fssw(1, hsl(0.75, 0.8, 0.6), 2) #.f(hsl(0.75, 0.8, 0.6)) .f(1) #.fssw(-1, 1, 6) .t(0, 80) #.blendmode(BlendMode.Cycle(13)) .chain(shake(8, 2, seed=f.i//2)) .chain(phototype(f.a.r, 3, 90, fill=hsl(0.75, 0.8, 0.8))) .blendmode(BlendMode.Cycle(20)) .chain(filmjitter(f.e("l", 4, cyclic=0), 3)) )) ================================================ FILE: examples/animations/ives.py ================================================ from coldtype import * from coldtype.fx.skia import phototype VERSIONS = { 2: dict(), 3: dict(), 4: dict(), 5: dict(), 6: dict(), 7: dict(), 8: dict(), 9: dict(), 10: dict(), 11: dict(), 12: dict(), 13: dict(), 14: dict(), 15: dict(), 16: dict(), 17: dict(), 18: dict(), 19: dict(), 20: dict(), 21: dict(), 22: dict(), 23: dict(), 24: dict(), 25: dict(), 26: dict(), } #/VERSIONS d = __VERSION__["key"] letters = "COLDTYPE"*4000 rsx = random_series(-80, 80, d) rsy = random_series(-80, 80, d) rsw = random_series(seed=d) rswd = random_series(seed=d) rsr = random_series(0, 10, d) rsa = random_series(-30, 30, d) @animation((1080, 1080), bg=1, tl=100) def grid_ƒVERSION(f): s = Scaffold.AspectGrid(f.a.r.inset(30), d, d, "N") return (P().enumerate(s, lambda x: StSt(letters[x.i], Font.MuSan(), f.adj(rsa[x.i]).e("eeio", 3, rng=(100, 500)), wght=rsw[x.i], wdth=rswd[x.i]) .align(x.el) .rotate(int(rsr[x.i])*180+f.adj(rsa[x.i]).e("eeio", 2, rng=(0, 270)), x.el.pc) .translate(rsx[x.i], rsy[x.i]) .intersection(P(x.el)) .f(1)) .insert(0, s.borders().fssw(-1, 1, 5)) .ch(phototype(f.a.r, 2, 127, 18, fill=bw(0)))) ================================================ FILE: examples/animations/letters_easing.py ================================================ from coldtype import * # inspired by https://github.com/coldtype/coldtype/discussions/135 letters = "".join([f"{x} " for x in "abcdefg<"]) at = AsciiTimeline(20, 30, "<\n"+letters).inflate() ease_curve1 = P().withRect(1000, lambda r, p: p .moveTo(r.psw) .boxCurveTo(r.pc, "NW", (.95, .65)) .boxCurveTo(r.pne, "SE", (.75, .95))) ease_curve2 = P().withRect(1000, lambda r, p: p .moveTo(r.psw) .boxCurveTo(r.pc, "NW", (.95, .95)) .boxCurveTo(r.pne, "SE", (.65, .135))) @animation((1080, 540), timeline=at, bg=1) def letters_easing(f): l = at.current(0).now() e1 = l.e(ease_curve1, 0) e2 = l.e(ease_curve2, 0, rng=(-360, 360)) w = f.a.r.w + 140 return (P( ease_curve1.copy() .fssw(-1, hsl(0.6, a=0.5), 4) .scale(0.5, point=(0,0)) .align(f.a.r), ease_curve2.copy() .fssw(-1, hsl(0, a=0.5), 4) .scale(0.5, point=(0,0)) .align(f.a.r), StSt(l.name, Font.RecMono(), 20) .align(f.a.r.take(100, "N")), StSt(l.name.upper(), Font.RecMono(), 250) .align(f.a.r) .f(0) .insert(0, lambda p: P(p.ambit().inset(2)) .f(hsl(0.3, a=0.5))) .t(-w/2+w*e1, 0) .rotate(e2))) ================================================ FILE: examples/animations/linewise.py ================================================ from coldtype import * txt = "COLD\nTYPE" @animation((1080, 540), tl=60) def scratch1(f): return (Glyphwise(txt, lambda x: Style(Font.ColdObvi(), 200 , wdth=f.adj(x.l*7).e("eeio"))) .lead(10) .xalign(f.a.r) .align(f.a.r)) @animation((1080, 540), tl=60, layer=0) def scratch2(f): return (P().enumerate(txt.splitlines(), lambda x: StSt(x.el, Font.ColdObvi(), 200 , wdth=f.adj(x.i*7).e("eeio"))) .stack(10) .xalign(f.a.r) .align(f.a.r)) ================================================ FILE: examples/animations/midi_cc.py ================================================ from coldtype import * mt = MidiTimeline(ººsiblingºº("media/midi_cc.mid")) wav = ººsiblingºº("media/midi_cc.wav") fnt = Font.MutatorSans() @animation(tl=Timeline(120, 30), bg=0, audio=wav) def cc(f): mt.hold(f.i) #print(mt.ci(104)) return (P( StSt("MIDI", fnt, 200 , wght=0, wdth=ez(mt.ci(102), "sei", rng=(0.5, 1))), StSt("CC", fnt, 300 , wght=1, wdth=ez(mt.ci(103), "eeo")), StSt("DATA", fnt, 100), ) .stack(20) .xalign(f.a.r) .align(f.a.r) .index(2, lambda p: p.scale(ez(mt.ci(104), "eeo", rng=(1, 5)))) .fssw(1, 0, 4, 1) .reverse()) ================================================ FILE: examples/animations/moire1.py ================================================ from coldtype import * from coldtype.raster import * from noise import pnoise1 r = Rect(1080) def patternmaker(): d = 50 s = Scaffold(Rect(1920)).numeric_grid(d, int(d)) return s.borders().s(1).ch(rasterized(s.r, wrapped=True)).align(r) pattern = freeze(1, 0, patternmaker) def postprocess(p): return P( P(r).f(hsl(0.20, 0.68, 0.75)), p.ch(phototype(r, 1.40, 148, 14, fill=hsl(0.60)))) @animation(bg=0, tl=Timeline(240*2, 18), composites=1) def scratch(f): last = f.last_render(λ.align(f.a.r).a(0.945)) n = pnoise1(f.e("l", 0, rng=(0, 14)), base=2, octaves=2) ro = n * -150 return (P( last, pattern.copy() .align(f.a.r) .rotate(ro, point=r.pc)) .postprocess(postprocess)) ================================================ FILE: examples/animations/officehours.py ================================================ from coldtype import * from coldtype.timing.nle.ascii import AsciiTimeline at = AsciiTimeline(1, """ < [L ] [Er ] [F ] [H ] """) @animation(timeline=at) def officehours(f): stx = Style.StretchX(0, L=(730*at.ki("L").io(10, "eeio"), 210), F=(1220*at.ki("F").io(10, ["qeio", "eleio"]), 260), H=(1900*at.ki("H").io(10, "ceio"), 250)) txt = (StSt("Coldtype\nOffice\nHours".upper() , ["MDNichrome.*Dark", Font.MutatorSans()] , fontSize=125 , ss03=1 , mods=stx , leading=20) .f(1) .xalign(f.a.r) .align(f.a.r) .t(0, 150)) bg = (P(txt.ambit().inset(-50)) .f(hsl(0.35, 0.8, 0.3))) # after bg, so it doesn't effect bounds txt.index([0, -1], lambda p: p .rotate(at.ki("Er").e("ceio", 0, r=(0, -360*2)))) date = (StSt("X/X, XX:00 UTC", "MDNichrome.*R", 80) .align(f.a.r.take(0.3, "S"))) return P( bg.copy().translate(5, -5).f(0), bg, txt, P(date.ambit().inset(-20)).f(0), date.f(1)) ================================================ FILE: examples/animations/original_demo.py ================================================ from coldtype import * states = [ dict(wdth=0, rotate=0), dict(wdth=1, rotate=15), dict(wdth=0.5, rotate=-210), dict(wdth=1, rotate=-25) ] spacings = [ dict(tu=300), dict(tu=80), dict(tu=330), dict(tu=150) ] at = AsciiTimeline(2, 30, """ < [0 ][1 ][2 ][3 ] """).shift("end", -10) @animation((1080, 1080/4), timeline=at, bg=1) def render(f): state = at.kf("eeio", keyframes=states) spacing = at.kf("seio", keyframes=spacings) return (StSt("COLDTYPE", Font.ColdtypeObviously(), 150, fill=0, **{**state, **spacing}, r=1, leading=80) .align(f.a.r) .f(0)) ================================================ FILE: examples/animations/penangle.py ================================================ from coldtype import * from coldtype.raster import phototype r = Rect(1080) ri = r.inset(160) p = P().m(ri.psw).ioc(ri.pne, 50).fssw(-1, 1, 10) def angler(e): return ez(e, "seio", 1, rng=(-90, -110)) def hatch(pt, a=-45, w=100, h=5): return (P() .rect(Rect.FromCenter(pt, w, h)) .rotate(a) .f(1)) samples = p.samples(7) curve = p.enumerate(samples, lambda x: hatch(x.el.pt, angler(x.e))).ch(phototype(r, 5, 90, 13)) @animation(bg=0, tl=Timeline(240, 48)) def scratch(f): return (P( curve, hatch(p.point_t(f.e("seio", 1))[0], angler(f.e("seio", 1)), 102, 20) .ch(phototype(f.a.r, 3, 120, 30, hsl(0.07, 0.90, 0.50))))) ================================================ FILE: examples/animations/physics2d.py ================================================ from coldtype import * from coldtype.fx.skia import phototype from coldtype.physics.pymunk import segments # pip install pymunk import pymunk from pymunk import Vec2d import random as rnd r = Rect(1080, 1080) ri = r.inset(-4, 0) txt = (StSt("C", Font.ColdObvi(), 1200, wdth=1) .align(r, ty=1) .fssw(-1, 1, 1)) space = pymunk.Space() space.gravity = (0.0, -900.0) # def begin(arbiter, space, data): # print("Collision started!") # return True # Allow the collision to occur # def pre_solve(arbiter, space, data): # print("Collision is being solved!") # return True # Continue processing the collision # def post_solve(arbiter, space, data): # print("Collision solved!") # # Use arbiter.total_impulse to get collision force if needed # def separate(arbiter, space, data): # print("Collision ended!") # Define the collision handler #handler = space.add_collision_handler(0, 0) # Attach callbacks to the handler #handler.begin = begin # handler.pre_solve = pre_solve # handler.post_solve = post_solve # handler.separate = separate static_body = space.static_body floor = pymunk.Segment(static_body, *ri.es.splat(), 0.0) static_lines = [ pymunk.Segment(static_body, *ri.ew.splat(), 0.0), pymunk.Segment(static_body, *ri.ee.splat(), 0.0), floor ] letter_lines = txt.ch(segments(static_body)) static_lines.extend(letter_lines) for line in static_lines: line.elasticity = 1 line.friction = 0.1 space.add(*static_lines) particles = [] @animation(tl=450, bg=0, render_bg=1, reset_to_zero=1) def scratch(f): if f.i == 275: space.remove(floor) for idx, s in enumerate(letter_lines): if idx%10 != 0: space.remove(s) if f.i <= 275: for _ in range(0, 10): mass = 0.1 radius = rnd.randint(7, 13) inertia = pymunk.moment_for_circle(mass, 0, radius, (0, 0)) body = pymunk.Body(mass, inertia) x = rnd.randint(10, f.a.r.w-10) body.position = x, rnd.randint(1150, 1300) shape = pymunk.Circle(body, radius, Vec2d(0, 0)) space.add(body, shape) particles.append(shape) particles_to_remove = [] for particle in particles: if particle.body.position.y < 0: particles_to_remove.append(particle) for particle in particles_to_remove: space.remove(particle, particle.body) particles.remove(particle) ### Update physics # Running in a range since that smaller increments repeated # seems to get better results for x in range(3): space.step(1 / 100.0) out = P() for particle in particles: p = Point(particle.body.position.x, particle.body.position.y) out += P().rect(Rect.FromCenter(p, particle.radius)).f(1) return out.ch(phototype(f.a.r.inset(0, 0), blur=1, cut=100, cutw=10)) ================================================ FILE: examples/animations/pixels.py ================================================ from coldtype import * from coldtype.raster import * drums = MidiTimeline(ººsiblingºº("media/cyber.mid"), bpm=100, fps=30) wav = download("https://coldtype.xyz/examples/media/cyber.wav", ººsiblingºº("media/cyber.wav")) @animation((1080, 1350) , timeline=drums , audio=wav , bg=hsl(0.95, 0.70, 0.55) , release=λ.export("h264", True, 4)) def pixels(f): def pixellate(idx, note, i, o, max, min): def _pixellate(p:P): value = drums.ki(note).adsr([i, o] , ["cei", "ceo"] , r=(max, min)) return p.index(idx, lambda _p: _p .ch(precompose(f.a.r, scale=value))) return _pixellate return (P( StSt("pixels\npixels\npixels", Font.JBMono() , fontSize=160 , wght=0.75 , wdth=1 , opsz=0) .f(1) .lead(60) .align(f.a.r, "C", ty=1) .mape(lambda e, line: line .declare(m:=e*0.0025) .index([1, 1], lambda p: p.t(0, drums.ki(42).adsr(r=(0, 40)))) .ch(pixellate(0, 36, 3, 40, 0.15-m, 0.01-m)) .ch(pixellate(2, 38, 3, 30, 0.15-m, 0.02-m)) .ch(pixellate(1, 42, 5, 20, 0.35-m, 0.05-m)) .ch(pixellate(3, 47, 5, 20, 0.15-m, 0.0125-m)) .ch(pixellate(4, 45, 5, 20, 0.15-m, 0.0125-m)) .ch(pixellate(5, 39, 5, 20, 0.15-m, 0.0125-m))) .index(1, λ.ch(phototype(f.a.r, 1, 90, 60, fill=1))) .index(2, λ.ch(phototype(f.a.r, 5, 150, 30, fill=1))))) ================================================ FILE: examples/animations/pseudomorph.py ================================================ from coldtype import * r = Rect(1440, 540) @animation(r, tl=70) def pseudomorph(f): return (StSt("HELLO" if f.e() < 0.5 else "WORLD" , Font.MutatorSans() , f.e(rng=(300, 100)) , wght=f.e() , wdth=f.e() , rotate=f.e(rng=(0, 360)) ) .align(f.a.r, tx=0)) ================================================ FILE: examples/animations/recursive_shape.py ================================================ from coldtype import * from coldtype.raster import * @animation((1080, 1080) , timeline=90 , composites=1 , bg=0 , release=lambda x: x.export("h264", loops=4)) def recursive_composite(f): def postprocess(result): return P( P(f.a.r).f(1), P().gridlines(f.a.r, 30, 30) .fssw(-1, hsl(0.65, 0.65, 0.95), 1), result.ch(luma(f.a.r, hsl(0.93, 1.00, 0.65)))) return (P( f.lastRender(lambda img: img .resize(0.997) .align(f.a.r) .translate(1, -3) ), (P(Rect(200, 200)) .align(f.a.r.inset(100, 100), "NW") .rotate(f.e("eeio", 0)*-360) .translate(f.a.r.w*0.6*f.e("ceio", 1), 0) .fssw(0, 1, 10) # invert for phototype )) .ch(phototype(f.a.r, fill=1, blur=3, cut=133, cutw=30)) .postprocess(postprocess)) ================================================ FILE: examples/animations/recursive_text.py ================================================ from coldtype import * from coldtype.fx.skia import phototype, luma @animation(timeline=80, bg=0 , composites=1 , render_bg=1) def recursive(f:Frame): last = f.last_render(lambda p: p.resize(0.95).align(f.a.r)) def postprocess(p): fill = hsl(f.e(1, rng=(0.95, 0.75)), 0.6, 0.6) return P( P(f.a.r).f(1), p.ch(luma(f.a.r, fill))) return (P( StSt("COLDTYPE", Font.ColdtypeObviously() , font_size=f.e(1, rng=(250, 20)) , wdth=f.e("ceio", 1, rng=(1, 0)) , tu=f.e(1, rng=(-150, 0)) , r=1) .align(f.a.r) .fssw(1, 0, 15, 1), StSt("Recursive", Font.RecursiveMono() , font_size=f.e("ceio", 1, rng=(1, 200)) , tu=f.e("ceio", 1, rng=(0, -100)) , r=1) .align(f.a.r) .fssw(1, 0, 15, 1) .visible(f.e(1) > 0.5)) .translate(0, f.e("eeio", 1, rng=(y:=390, -y))) .insert(0, last) .ch(phototype(f.a.r, blur=1, cut=123, cutw=35, fill=1)) .postprocess(postprocess)) release = recursive.export("h264", loops=4) ================================================ FILE: examples/animations/retails/casual.py ================================================ from coldtype import * at = AsciiTimeline(6, 30, """ < [0 ][1 ][2 ][3 ][4 ] [0w ] [1w ] [2w ] [3w ] [4w ] [z ] [o ] """) @animation((1080, 1080), timeline=at, bg=1, render_bg=1) def casual1(f): letter = at.ki(list("012345")) li, lidx = letter.e(find=1) z = at.ki("z") o = at.ki("o") return (Glyphwise("Dicey", lambda g: Style("Casual", 100 , opsz=o.e("eeo", 1, r=(1, 0)) if o.on() else at.ki(g.i).e("eeio", 1, r=(1, 0)) , wght=z.e("eeo", 0, r=(1, 0)) if z.on() else at.ki(f"{g.i}w").io([8, 8], "eeo"))) .align(f.a.r) .centerPoint(f.a.r, (lidx, "txtyC"), i=li) .cond(True, lambda p: p .scale(letter.io([15, 5], r=(1,10)), pt=(lidx, "tyC")) .scale(z.io([10, 15], ["eeo", "eei"], r=(1, 5))), lambda p: p .scale(letter.e(r=(1,10)), pt=(lidx, "tyC")) .scale(z.e("eeio", r=(1, 5)))) .f(0)) release = casual1.export("h264", loops=3) ================================================ FILE: examples/animations/retails/chopper.py ================================================ from coldtype import * from coldtype.raster import * from functools import partial def Chopper(**kwargs): d = {} def chopper(n, s, e, w, i, p): amb = p.ambit(tx=0, ty=0) return p.intersection(P(amb.drop(n, "N", forcePixel=1).drop(s, "S", forcePixel=1).drop(e, "E", forcePixel=1).drop(w, "W", forcePixel=1))).translate(-w, 0)#.record(P(amb).outline(2).reverse()) for k, v in kwargs.items(): d[k] = (-(v[2]+v[3]), partial(chopper, v[0], v[1], v[2], v[3])) return d rsc = random_series() @animation((1080, 1080), tl=160, bg=hsl(0.7, 0.6)) def chopper(f): def word(f): e = f.io([20, 20]) ##d = 140*f.e("eeo") stx = Chopper( O=(0,0,120*e,120*e), C=(0,0,140*e,110*e), M=(0,0,100*e,80*e), P=(0,0,140*e,117*e), R=(0,0,120*e,180*e), E=(0,0,80*e,140*e), S=(0,0,140*e,140*e), ) return (StSt("COMPRESS".upper() , "Hex Franklin v0.3 Tyght V" , fontSize=f.io([50, 40], ["eeio", "eeio"], rng=(180, 900)) , NOTC=1 , TYTE=0 , mods=stx , tu=f.io([60, 60], ["eeio", "eeio"], rng=(300, 10)) , kp={ "O/M":-10*e, "M/P":-11*e, "R/E":-7*e, "P/R":-6*e, "E/S":9*e } #, wght=0.75 , wdth=0 #, tu=10 ) .mapv(lambda i, p: p.f(hsl(rsc[i], 0.5)).up().___insert(0, lambda x: P(x.ambit()).fssw(hsl(rsc[i], 0.5, 0.8, 0.3), 0, 1))) #.track(10) #.align(f.a.r) .f(1) #.ch(shake(2, 1, seed=f.i//4)) ) return (P( word(f), word(f.adj(-(d:=0))), word(f.adj(-d*2)), word(f.adj(-d*3)), word(f.adj(-d*4)), ) .xalign(f.a.r) .stack(f.e("eeio", rng=(60, 24))) .align(f.a.r) .ch(filmjitter(f.e("l"), scale=(1,1))) .ch(phototype(f.a.r, 1.5, 180, 40, fill=1))) release = chopper.export("h264", True, 4) ================================================ FILE: examples/animations/retails/colorfont.py ================================================ from coldtype import * ppvf = Font.Find("PappardelleParty-VF") custom_palette = [ hsl(0.35, 0.7), hsl(0.5, 0.7), hsl(0.1, 0.7), hsl(0.9, 0.7) ] def spin(fa, g): y = 100 # address color font layers individually g[2].translate(0, fa.e(1, rng=(0, y))) g[0].translate(0, fa.e(1, rng=(0, -y))) g[1].rotate(fa.e(0, rng=(0, -360*2))) @animation((1080, 1080), timeline=120) def pappardelle(f): wave = (StSt("SPIN", ppvf, 500, palette=custom_palette, #palette=1, SPIN=f.e("l", 0)) .align(f.a.r)) r_wave = wave.ambit(tx=1, ty=1) return P( P(r_wave.inset(-20, -15)).f(None).s(custom_palette[2]).sw(3), (wave.copy() .map(lambda i, p: spin(f.adj(-i*4), p)) .rotate(f.e(0, to1=1)*360, point=r_wave.pc))) ================================================ FILE: examples/animations/retails/digestive_snake.py ================================================ from coldtype import * r = Rect(1920, 500) track = (P() .vl([ ('moveTo', ((-342, 68),)), ('curveTo', ((-162, 68), (-108, 217), (140, 217.0))), ('curveTo', ((381, 217), (454, 98), (639, 98))), ('curveTo', ((843, 98), (925, 217), (1106, 217))), ('curveTo', ((1322, 217), (1426, 68.0), (1656, 68))), ('endPath', ())]) .scale(0.25) .fssw(None, 1, 1) .repeat(3) .align(r) .translate(0, -20)) def ds(e1): # 52.98 is a magic number for this string # uncomment the 119-rendered static at the bottom # to calibrate for a different string/font return StSt("Digestive", "Digestive", 52.98, wdth=e1, ro=1) minw = ds(0).ambit(tx=1).point("SE").x maxw = ds(1).ambit(tx=1).point("SE").x def render_snake(f): e, l = f.e("eeio", 2, loop_info=1) dps = ds(e) offset = 0 if l in [1, 3]: offset = maxw - dps.ambit(tx=1).point("SE").x offset += math.floor(l/2)*(maxw-minw) dps.distributeOnPath(track, offset=offset) return dps @animation(rect=(1920,500), timeline=120, bg=hsl(0, l=0.97)) def render(f): now = render_snake(f) return (P( P(f.a.r).scale(0.3).fssw(-1, -1, 2), P(f.a.r).scale(0.25).fssw(-1, -1, 2), (track.copy() .fssw(None, hsl(0.65, l=0.9), 15) .translate(0, -8)), #render_snake(Frame(119, f.a)).f(0), now.f(hsl(0.9, l=0.6, s=0.7))) .translate(0, -30) .scale(4.1) .align(f.a.r)) ================================================ FILE: examples/animations/retails/digestive_wind.py ================================================ from coldtype import * import noise # available from https://ohnotype.co/fonts/digestive # though other variable-wdth faces can be substituted df = Font.Find("DigestiveVariable") t = Timeline(180, storyboard=[0]) # TODO must be a way to speed this way up by inferring a height axis from the width def style_a(f, hit): ps:PS = (StSt("Digestive", df, 300+30*(1-hit), wdth=0, ro=1, t=-10*(1-hit)) .align(f.a.r)) def alter(idx, p): fr = p.ambit() rng = 10+45*hit factor = 0.05 x_seed = (f.i+idx)*factor fs = (200-rng)+noise.pnoise1(x_seed, repeat=int(t.duration*factor))*rng if p.glyphName != "space": return StSt(p.glyphName, df, fs, wdth=1, ro=1, fit=fr.w).align(fr)[0] else: return P() return ps.map(alter) @animation(rect=(1200,300), timeline=t, bg=0) def render(f): return style_a(f, 1).f(1) ================================================ FILE: examples/animations/retails/gridsystems.py ================================================ from coldtype import * author = "Josef Müller-Brockmann" publisher = "Niggli" txt_en = [ "Grid systems", "in graphic design", "A visual communcation manual\nfor graphic designers,\ntypographers and\nthree dimensional designers" ] txt_de = [ "Raster systeme", "für die\nvisuelle Gestaltung", "Ein Handbuch für\nGrafiker, Typografen und\nAusstellungsgestalter" ] @renderable(Rect(215*5, 300*5), bg=hsl(0.015, 0.6, 0.555)) def cover(r): s = Scaffold(r.inset(40)).numeric_grid(4, 8, 30, 30) s1 = Style("GrossV", 160, wght=0.55, wdth=0.91) s2 = Style("GrossV", 37, wght=0.25, wdth=0.91) return (P( s.borders().fssw(-1, 1, 0.5), P(s).fssw(-1, 1, 0.25), s.view(fill=False, vectors=True).f(bw(1, 0.25)) if 1 else None, P( StSt(author, s2).f(0).align(s["2|7"], "NW"), StSt(txt_en[0], s1).align(s["0|5"], "NW"), StSt(txt_en[1], s2).align(s["0|4"], "NW"), StSt(txt_en[2], s2).align(s["2|4"], "NW"), StSt(txt_de[0], s1).align(s["0|3"], "NW"), StSt(txt_de[1], s2).align(s["0|2"], "NW"), StSt(txt_de[2], s2).align(s["2|2"], "NW"), StSt(publisher, s2).align(s["2|0"], "SW", ty=1) ).f(0))) ================================================ FILE: examples/animations/retails/hansjorg.py ================================================ from coldtype import * from coldtype.raster import * from string import ascii_lowercase from random import Random # adapted from Maurice Meilleur’s adaptation of a 1964 piece by Hansjörg Mayer futura = Font.Find("PolymathV") ln = len(ascii_lowercase)-2 def scrambled(seed, split): r = Random(seed) xs = ''.join(r.sample(ascii_lowercase, len(ascii_lowercase))) return f"{xs[:-split]}\n{xs[-split:]}" @animation(1920 , bg=hsl(0.11, 0.70, 0.65) , tl=Timeline(ln*8, 12) , release=λ.export("h264", loops=2)) def hj(f:Frame): s = Scaffold(f.a.r.inset(80)).numeric_grid(1, 17) e1 = "l" e2 = "ceio" factor = f.e("seio", 2, rng=(1, 2)) def row(x): right = round(f.adj(x.i*factor).e(e2, 4, rng=(len(ascii_lowercase)-1, 1))) a = f.adj(-x.i*factor) xs = scrambled(x.i-f.i, right) return (StSt(xs, futura , fontSize=a.e(e1, rng=(50, 150)) , tu=a.e(e1, rng=(120, -150)) , wght=a.e(e1, rng=(0.65, 1)) , opsz=a.e(e1, rng=(0, 1)) , liga=0 , slig=0) .î(0, λ.align(x.el, "W", tx=0)) .î(1, λ.align(x.el, "E", tx=0)) .ch(filmjitter(f.e("l") , scale=(10, 10) , base=x.i))) return (P().enumerate(s, row) .f(1) .ch(phototype(f.a.r , blur=1.5 , cut=f.e(e1, rng=(140, 50)) , cutw=40 , fill=0))) @animation(1080 , tl=hj.timeline , solo=1 , release=λ.export("h264", loops=2)) def hj_resize(f): return hj.pass_img(f.i).resize(0.5625) ================================================ FILE: examples/animations/retails/montreuil.py ================================================ from coldtype import * from coldtype.raster import * from functools import partial from random import Random fnt = Font.Find("MontreuilPlay") import numpy as np import random txt = "CLAWHAMMER" options = [ [1, 2], [1, 2, 3, 4, 5], [3, 4, 5], [3, 4, 5, 2], [3, 4, 5], [3, 4, 5, 6], [3, 4, 5], [3, 4, 5], [3, 4, 5], [3, 4, 5, 6, 7], [6, 7] ] repeats = len(options) def generate_matrix_sequence(changes_per_step): np.random.seed(1) random.seed(1) current_matrix = np.random.randint(1, 3, size=(repeats, len(txt))) for idx, x in enumerate(current_matrix): for jdx, _ in enumerate(x): choices = options[idx] weights = np.array([0.01] + [0.95/(len(choices)-1)] * (len(choices)-1)) current_matrix[idx, jdx] = random.choices(options[idx], weights=weights)[0] #if random.random() < 0.05: # current_matrix[idx, jdx] = 4 unvisited = set((i, j) for i in range(repeats) for j in range(len(txt))) matrices = [current_matrix.copy()] while unvisited: num_changes = min(changes_per_step, len(unvisited)) cells_to_change = random.sample(list(unvisited), num_changes) new_matrix = current_matrix.copy() for i, j in cells_to_change: choices = options[i] weights = np.array([0.01] + [0.95/(len(choices)-1)] * (len(choices)-1)) current_val = current_matrix[i, j] new_val = random.choice([x for x in choices if x != current_val]) if random.random() < 0.01: new_val = 0 new_matrix[i, j] = new_val unvisited.remove((i, j)) matrices.append(new_matrix) current_matrix = new_matrix return matrices matrices = generate_matrix_sequence(2) matrices.append(matrices[-1].copy()) matrices.append(matrices[-1].copy()) matrices.append(matrices[-1].copy()) matrices.append(matrices[-1].copy()) matrices.insert(0, matrices[0].copy()) matrices.insert(0, matrices[0].copy()) matrices.insert(0, matrices[0].copy()) @animation(bg=0, tl=Timeline(len(matrices)*2, 24)) def scratch(f:Frame): _r = Rect(1000) io = P().m(_r.psw).l(_r.pse.o(-400, 0)).ioc(_r.pne.o(-100, 0), 0).l(_r.pne).fssw(-1, 1, 2) #return io def mp(line): def character(x): style = matrices[int(f.e("l", 1, (0, len(matrices)-1)))][line.i][x.i] return (StSt(x.el, fnt, 100, features={f"ss0{style}":1})) return P().enumerate(txt, character).spread(0) #_mp = partial(mp, 100, "GOODHERTZ") return (P( # _mp(0, [0, 1, 2]), # _mp(1, [5, 3, 4]), # _mp(2, [5, 3, 4]), # #_mp([6]), # _mp(7, [0, 3, 4, 5]), # #_mp([0]), # _mp(8, [0, 3, 4]), # _mp(10, [0, 3, 4]), # _mp(11, [0, 3, 4]), # #_mp([3, 4]), # #_mp([3, 4]), # _mp(9, [0, 3, 4, 5]), # #_mp([3, 4, 5]), # _mp(3, [6, 7]), ) .enumerate(range(0, repeats), mp) #.f(hsl(0.3, 0.90, 0.70)) #.map(lambda p: p.insert(0, P(p.ambit(tx=0, ty=0)).f(hsl(0.70, 0.90, 0.70)))) #.index(2, lambda p: p.t(-10, 0)) #.stack(f.e(io, 1, (18, 22)), ty=0) .stack(18, ty=0) .align(f.a.r, tx=0, ty=0) .collapse() #.pen().ro().fssw(-1, 1, 4) .f(1) #.fssw(-1, 1, 1) .scale(0.9) .ch(phototype(f.a.r, 1.5 , 150, 50)) ) release = scratch.gifski(open=True) ================================================ FILE: examples/animations/retails/stacked_and_justified.py ================================================ from coldtype import * from coldtype.fx.skia import phototype fatface = Font.Find("OhnoFatfaceV") @animation(timeline=Timeline(100, storyboard=[0]), bg=0) def render(f): c1, c2 = (f.a.r .inset(0, 50) .divide(f.e(1, rng=(0.15, 0.85)), "N") .map(lambda p: p.inset(20, 5))) s = Style(fatface, t=-25, wdth=1, wght=1, ro=1, r=1) return (P( (StSt("STACKED &", s.mod(fitHeight=c1.h, opsz=f.e(1)) , fit=c1) .align(c1) .trackToRect(c1.inset(f.e(1, rng=(30, 0)), 0), pullToEdges=1, r=1)), (StSt("JUSTIFIED", s.mod(fitHeight=c2.h, opsz=f.e(1, rng=(1, 0))) , fit=c1) .align(c2) .trackToRect(c2, pullToEdges=1, r=1))) .fssw(1, 0, 11, 1) .sm(0.99) .ch(phototype(f.a.r, blur=2, cut=150, cutw=25))) ================================================ FILE: examples/animations/retails/vulfbach.py ================================================ from coldtype import * from coldtype.fx.skia import color_phototype """ Run as `coldtype examples/animation/vulfbach.py` To multiplex, add ` -m` to the end of that call (then when you hit `a` in the viewer app, the frames will render in parallel in random-order) Rendered with organ.wav as the backing track: https://vimeo.com/489013931/cd77ab7e4d """ midi = MidiTimeline( "examples/animations/media/organ.mid" , bpm=183, fps=30) note_width = 3 r = Rect(1440, 1080) def pos(x, y): y = int(y) return (x*note_width, (y-midi.min)*(r.h-200)/midi.spread+100) def build_line(): dp = P().f(None).s(rgb(1, 0, 0.5)).sw(3) last:Timeable = None for t in midi.timeables: if last and (t.start - last.end > 3 or last.name == t.name): dp.lineTo((pos(last.end, last.name))) dp.endPath() last = None if last: if last.end < t.start: dp.lineTo((pos(last.end, last.name))) else: dp.lineTo((pos(t.start, last.name))) dp.lineTo((pos(t.start, t.name))) else: dp.moveTo((pos(t.start, t.name))) last = t if last: dp.lineTo((pos(last.end, int(last.name)))) dp.endPath() return dp line = build_line() @animation(timeline=midi, rect=r) def render(f): time_offset = -f.i * note_width + r.w - note_width * 3 time_offset += 10 # fudge looped_line = P( (line.copy() .translate( time_offset - f.t.duration * note_width, 0)), (line.copy() .translate(time_offset, 0))) return (P( P().rect(f.a.r).f(0), (looped_line.pen() .ch(color_phototype(f.a.r, blur=20, cut=215, cutw=40))), (looped_line.pen() .ch(color_phototype(f.a.r, blur=3, cut=200, cutw=25))))) ================================================ FILE: examples/animations/retails/wavinghand.py ================================================ from coldtype import * @animation() def peace(f): txtfont = "Degular-Black" return (StSt("Peace ✌ !", txtfont, 100, space=230) .replaceGlyph(".notdef", StSt("✌", "Gooper", 100)) .align(f.a.r) .findGlyph("uni270C", lambda p: p .translate(3, -5) .rotate(f.e("seio", r=(-20, 20)))) .f(0)) ================================================ FILE: examples/animations/retails/welcome.py ================================================ from coldtype import * @animation((1080, 290), timeline=Timeline(90), render_bg=True, bg=hsl(0.65)) def welcome(f): return (Glyphwise("ABC", lambda g: Style("CheeeVar", 250, tu=f.e("eeio", 1, rng=(0, -350)), yest=f.e("ceio", 1), grvt=f.adj(g.i*10).e("seio", 3))) .fssw(-1, 1, 3) .align(f.a.r, ty=1)) release = welcome.export("gif") ================================================ FILE: examples/animations/rgbsplit.py ================================================ from coldtype import * from coldtype.raster import * @animation(bg=0, tl=60) def rgbsplit(f): return (StSt("COLD\nTYPE", Font.ColdObvi() , multiline=1 , fontSize=f.e(rng=(600, 360)) , ro=1 , rotate=f.e("eei", rng=(10, 0)) , wdth=f.e("eei") , leading=f.e("eeo", rng=(10, 50)) , tu=f.e("eeio", rng=(-70, 0))) .xalign(f.a.r) .align(f.a.r) .reverse(recursive=1) .layer( lambda p: p.t(d:=f.e(rng=(20, 30)),-d).f("g"), lambda p: p.fssw(-1, "b", 20), lambda p: p.fssw("r", "b", 6)) .ch(rgbmod(f.a.r , r=lambda x: x .ch(filmjitter(f.e("l", 0), 0, scale=(5, 6))) .ch(phototype(f.a.r, 1.5, 120, 45, fill=hsl(1.90,0.90,0.75))) , g=lambda x: x .ch(filmjitter(f.e("l", 0), 1, scale=(5, 6))) .ch(phototype(f.a.r, 3, 170, 15, fill=hsl(0.37,0.8,0.75)))))) ================================================ FILE: examples/animations/roundandround.py ================================================ from coldtype import * from coldtype.raster import phototype, filmjitter e1 = (P().withRect(Rect(1000), lambda r, p: p .m(r.psw) .ioc(r.pc, 10, 30) .ioc(r.pne, 10, 30) .ep())) e2 = (P().withRect(Rect(1000), lambda r, p: p .m(r.psw) .ioc(r.pn, 10, 30) .ioc(r.pse, 10, 30) .ep())) e3 = (P().withRect(Rect(1000), lambda r, p: p .m(r.psw) .ioc(r.pc, 30, 30) .ioc(r.pne, 30, 30) .ep())) @renderable(1000, bg=1, solo=-1) def easer(r): return P(e1, e2, e3).fssw(-1, 0, 1) @animation(bg=hsl(0.07, 0.8, 0.50), tl=Timeline(180, 18), release=λ.export("h264", loops=2)) def rounder(f): j = f.e("seio", 1, rng=(7, 6)) h = 500 def setter(x): nonlocal h fs = 32 - x.i*0.58 h -= (fs) #el = StSt("S", Font.MuSan(), fs+12, wght=1, wdth=1-x.e).layer(21) el = P(Rect(fs*(1-(x.e*0.9)), fs)).layer(21) return (el .align(f.a.r) .t(0, h) .f(0) .unframe() .map(lambda i, p: p .rotate(-i * 360/len(el), point=f.a.r.pc) .ch(filmjitter(f.adj(-x.i).e("l", 0), x.i+i, scale=(j,j)))) .align(f.a.r) .rotate(f.e(e1, 0, rng=(0, f.e(e2, 0, rng=(0, -x.i*85.10)))))) return (P().enumerate(range(0, 14), setter) .f(1) .rotate(f.e(e3, 0, rng=(0, -360))) .ch(phototype(f.a.r , blur=f.e(e2, 0, rng=(12, 1)) , cut=f.e(e2, 0, rng=(60, 183)) , cutw=f.e(e2, 0, rng=(3, 43)) , fill=hsl(0.17, 1.00, 0.70)))) ================================================ FILE: examples/animations/separation.py ================================================ from coldtype import * # variation on a design by @mjmeilleur @animation(tl=240, bg=1) def separation(f): e, info = f.e("eeio", 4, loop_info=True) seed = info//2 extent = 160 + 100*seed rx = random_series(-extent, extent, seed=1+seed) ry = random_series(-extent, extent, seed=2+seed) rs = random_series(0, seed, seed=4+seed) rr = random_series(-360*2, 360*2, seed=3+seed) s = Scaffold(f.a.r.inset(300)).numeric_grid(4) return (P().enumerate(s, lambda x: P() .rect(x.el.r.inset(12)) .outline(4)) .layer( lambda p: p.f(hsl(0.9, 0.8)), lambda p: p.f(hsl(0.17, 0.8, 0.6)), lambda p: p.f(hsl(0.65, 0.8, 0.6))) .collapse() .map(lambda i, p: p .t(rx[i]*e, ry[i]*e) .scale(1+rs[i]*e) .rotate(rr[i]*e)) .blendmode(BlendMode.Cycle(13))) ================================================ FILE: examples/animations/simple_recording.json ================================================ {"cursor": {"0": [576, 78], "1": [549, 84], "2": [510, 84], "3": [453, 96], "4": [405, 124], "5": [397, 146], "6": [406, 170], "7": [441, 188], "8": [477, 196], "9": [511, 198], "10": [554, 211], "11": [609, 242], "12": [624, 285], "13": [601, 326], "14": [550, 334], "15": [480, 313], "16": [425, 299], "17": [400, 293], "18": [330, 306], "19": [301, 348], "20": [300, 383], "21": [330, 411], "22": [377, 435], "23": [433, 447], "24": [460, 450], "25": [535, 468], "26": [575, 508], "27": [584, 549], "28": [529, 570], "29": [452, 559], "30": [378, 538], "31": [298, 544], "32": [278, 593], "33": [314, 641], "34": [374, 674], "35": [445, 678], "36": [511, 673], "37": [556, 686], "38": [547, 740], "39": [504, 795], "40": [453, 808], "41": [391, 808], "42": [280, 802], "43": [246, 837], "44": [249, 864], "45": [303, 925], "46": [427, 987], "47": [592, 974], "48": [687, 854], "49": [685, 772], "50": [675, 718], "51": [723, 656], "52": [804, 654], "53": [827, 718], "54": [812, 780], "55": [760, 770], "56": [727, 712], "57": [730, 589], "58": [763, 454], "59": [842, 369], "103": [948, 199], "104": [943, 172], "105": [916, 139], "106": [884, 115], "107": [821, 93], "108": [783, 87], "109": [747, 84], "110": [711, 82], "111": [685, 83], "112": [669, 83], "113": [654, 82], "114": [639, 80], "115": [626, 79], "116": [610, 79], "117": [597, 79], "118": [590, 78], "119": [581, 78], "60": [891, 398], "61": [908, 446], "62": [886, 505], "63": [862, 530], "64": [839, 533], "65": [820, 500], "66": [803, 448], "67": [798, 378], "68": [838, 331], "69": [890, 317], "70": [937, 364], "71": [959, 489], "72": [893, 669], "73": [741, 750], "74": [565, 760], "75": [369, 694], "76": [315, 611], "77": [330, 564], "78": [407, 518], "79": [520, 492], "80": [631, 501], "81": [685, 557], "82": [673, 624], "83": [639, 655], "84": [577, 661], "85": [537, 645], "86": [510, 595], "87": [517, 537], "88": [575, 426], "89": [676, 382], "90": [771, 384], "91": [798, 440], "92": [769, 481], "93": [728, 470], "94": [718, 421], "95": [728, 369], "96": [769, 333], "97": [771, 332], "98": [813, 311], "99": [859, 290], "100": [895, 269], "101": [922, 247], "102": [939, 229]}} ================================================ FILE: examples/animations/simplevarfont.py ================================================ from coldtype import * @animation((1920, 540), timeline=Timeline(60, 30), bg=1) def unfold(f): return (StSt("Coldtype".upper() , Font.ColdObvi() , fontSize=f.e("eeio", rng=(100, 300)) , wdth=f.e("eeio") , tu=f.e("eeio", rng=(-130, 100)) , ro=1) .align(f.a.r) .mapv(lambda i, p: p .rotate(360*f.adj(-i*0.25).e("eeio", 1))) .reverse() .fssw(hsl(0.7, a=0.75), 0, 10, 1)) ================================================ FILE: examples/animations/slicer.py ================================================ from coldtype import * from coldtype.fx.skia import phototype rs = random_series(-(r:=50), r, seed=0) @animation((1080, 1080), bg=0, tl=120) def slicer(f): s = Scaffold(f.a.r.inset(-500, 0)).grid(21, 1) txt = (StSt("COLD\nTYPE", Font.ColdObvi(), f.e(r=(180, 440)), wght=1, leading=50) .xalign(f.a.r) .align(f.a.r, ty=1) .pen() .removeOverlap()) skew = 0.25 return (P().enumerate(s, lambda x: P(x.el.rect.inset(2)) .skew(skew, 0, pt=(0, 0)) .intersection(txt) .translate(rs[x.i]*skew, rs[x.i]) .f(1) .ch(phototype(f.a.r, 1.5, 180, 35)))) ================================================ FILE: examples/animations/sonification.py ================================================ from coldtype import * import wave, struct VERSIONS = { "C": dict(text="C", font="ObviouslyV"), "O": dict(text="O", font="ObviouslyV"), "L": dict(text="L", font="ObviouslyV"), "D": dict(text="D", font="ObviouslyV"), } #/VERSIONS """ Run in terminal: `coldtype examples/animations/sonification.py` After a build call (aka hitting the `b` key in the viewer app), this code will render individual wave files for each letter, to the examples/animations/renders/sonification folder Those waves can then be played back in any DAW and should be visible on an x/y scope (like this one http://goodhertz.com/midside-matrix) """ class sonification(animation): def __init__(self, timeline, filename, samples_per_frame=1, **kwargs): self.filename = filename self.samples_per_frame = samples_per_frame super().__init__(fmt="pickle", timeline=timeline, **kwargs) def build_wav(self): sampleRate = 48000.0 # hertz obj = wave.open(str(self.output_folder.parent / self.filename), 'w') obj.setnchannels(2) obj.setsampwidth(2) obj.setframerate(sampleRate) for i in range(0, tl.duration): res = (self.func(Frame(i, self)) .scale(-1, 1) .rotate(-45) .translate(-500, -500) .removeOverlap() .flatten(1)) left, right = [], [] for (_, pts) in res._val.value: if len(pts) > 0: left.append(pts[0][0]) right.append(pts[0][1]) for _ in range(0, self.samples_per_frame): for idx, l in enumerate(left): data = struct.pack(' 0: return p.difference(line[i-1].copy().outline(10, drawOuter=False)) letters = (StSt("COLD\nTYPE", Font.ColdObvi(), 300 , tu=-200 , ro=1) .map(lambda p: p.map(partial(cut, p)))) curve = (P().withRect(1000, lambda r, p: p .m(r.psw) .ioc(r.pn, 30, -10) .ioc(r.pse, 0, 50) .ep())) @animation(r, tl=Timeline(60), bg=0) def understroke_cut(f): return (letters .copy() .map(lambda p: p .track(f.e(curve, 0, rng=(0, 150)))) .align(f.a.r) .f(1)) ================================================ FILE: examples/animations/truchet.py ================================================ from coldtype import * from coldtype.fx.skia import phototype # inspired by https://mauricemeilleur.net/truchet_tiles at = AsciiTimeline(8, 30, """ < [0 ] [0 ] [1 ] [1 ] [2 ] [2 ] """) eases = ["beo", "eeo", "ceo"] colors = [hsl(0.17, 0.8), hsl(0.6, 0.8), hsl(0.95, 0.8)] rs = random_series(0, 3) rs2 = random_series() @animation(Rect(1000, 1000), tl=at, bg=0) def truchet1(f): s = Scaffold(f.a.r).numeric_grid(8) def rotate(i, p): (p.rotate(90*int(rs[i])) # initial .rotate(f.t.ki(i%3).ec(eases[i%3], rng=(0, 90)))) return (P(cr:=s[0].r) .difference(P().oval(cr).t(+cr.w/2).outline(1)) .difference(P().oval(cr).t(-cr.w/2).outline(1)) .xor(P(cr)) .f(1) .replicate(s.cells()) .map(rotate) .ch(phototype(f.a.r, blur=5, cut=20, cutw=6))) def release(passes): from coldtype.renderable.animation import gifski gifski(truchet1, passes) print("/release") ================================================ FILE: examples/animations/truchet3.py ================================================ from coldtype import * from coldtype.blender import * from random import Random # inspired by https://mauricemeilleur.net/truchet_tiles rs = random_series(0, 3, seed=0) rs2 = random_series() rs3 = random_series(0.5, 3.5) tn = 8 at = AsciiTimeline(10, 30, """ < [0 ] [1 ] [1 ] [0 ] [2 ] [2 ] [3 ] [3 ] [4 ] [4 ] [5 ] [5 ] < [6 ] [6 ] [7 ] [7 ] [8 ] [8 ] [9 ] [9 ] """) spins = [] for t in at.timeables: spins.append(Timeable(t.start+6, t.end-6, name=f"{t.name}_spin")) t2 = Timeline(timeables=[*at.timeables, *spins]) rnd = Random() rnd.seed(4) ridxs = list(range(0, tn*tn)) rnd.shuffle(ridxs) @b3d_runnable() def setup(bw:BpyWorld): (bw.deletePrevious(materials=False)) @b3d_animation(Rect(1000), tl=at, bg=None) def truchet1(f): t2.hold(f.i) def rotate(i, p): ri = ridxs[i]//6 (p.rotate(90*int(rs[i])) .rotate(t2.ki(f"{ri}_spin") .ec("eeio", (0, 90))) .ch(b3d(lambda bp: bp .extrude(0.25) .locate(z=t2.ki(f"{ri}") .e("eeio", rng=(0, rs3[i])))))) s = Scaffold(f.a.r).numeric_grid(tn) return (P(tr:=s[0].r.inset(0.05, 0.05)) .difference(P() .append(P().oval(tr).t(tr.w/2)) .append(P().oval(tr).t(-tr.w/2))) .f(0) .replicate(s.cells()) .map(rotate)) ================================================ FILE: examples/animations/twister.py ================================================ from coldtype import * from functools import partial def pair(tx, f, x): fa = f.adj(-x.i*3) ro = fa.e("eeio", 0, rng=(0, -360)) p = (P(f.a.r .take(350, "mdx") .take(30, "mny")) .fssw(1, 0, 5) .translate(0, x.i*10)) return P( p.copy().rotate(ro), p.copy() .rotate(-ro+270) .translate(tx, 0)) @animation((1080, 1080), timeline=120) def twister(f:Frame): tx = 250 return (P().enumerate(range(0, 30), partial(pair, tx, f)) .translate(-tx*0.5, 300) .reversePens()) ================================================ FILE: examples/animations/ulrich_e.py ================================================ from coldtype import * from coldtype.raster import * from coldtype.timing.easing import all_eases # e stands for easing # adapted from Maurice Meilleur’s adaption of a 1968 piece by Tim Ulrich r = Rect(1080) s = Scaffold(r.inset(20)).numeric_grid(29, gap=4, annotate_rings=True) img = (StSt("e", "neuehaas", 85) .f(1) .align(s[0].r, tx=1, ty=1) .insert(0, P(s[0].r) .f(hsl(0.08, 0.8, 0.6, a=0.0))) .ch(rasterized(s[0].r.inset(-10), wrapped=True))) @animation(1080, tl=60, bg=hsl(0.11, 0.80, 0.88), mute=0) def manye_live(f): return (P().enumerate(s.cells(), lambda x: img.copy() .declare( ring:=x.el.data("ring"), ring_e:=x.el.data("ring_e"), easer:=all_eases[ring]) .align(x.el.r, tx=1, ty=1) .rotate(ring_e*360+f.adj(-ring*8).e(easer, 0, r=(0, -360)))) .ch(phototype(f.a.r, 1.5, 120, 30, fill=0.1))) @animation(1080, tl=60, bg=0, mute=1) def manye_live2(f): def letter(x): ring = x.el.data("ring") ring_e = x.el.data("ring_e") easer = all_eases[ring] easer = "ceio" return (StSt("e", "PolymathV", 215, wght=f.adj(-ring*8).e(easer), opsz=1) .f(1) .align(x.el.r, tx=1, ty=1) .rotate(ring_e*360+f.adj(-ring*8).e(easer, 0, r=(0, -360)))) return (P().enumerate(s.cells(), letter) .ch(phototype(f.a.r, 1.5, 120, 30, fill=1))) ================================================ FILE: examples/animations/versioned.py ================================================ from coldtype import * VERSIONS = { "A": dict(text="COLD", font="ObviouslyV"), "B": dict(text="TYPE", font="ObviouslyV"), } #/VERSIONS @animation((1080, 1080/4), bg=1, tl=45, release=lambda a: a.gifski()) def versioned_ƒVERSION(f): return (StSt(__VERSION__["text"].upper() , __VERSION__["font"] , 100 , wght=f.e("eeio", 2) , wdth=f.e("eeio", 1)) .align(f.a.r)) ================================================ FILE: examples/animations/versioned_with_sidecar.py ================================================ from coldtype import * root = ººsiblingºº("../..").resolve() """ __VERSION__ is populated by VERSIONS defined in _versions.py versions of this .py """ @animation(bg=0) def scratch_ƒVERSION(f): return (P( StSt(__VERSION__["key"], Font.RecMono(), 72), StSt(str(__VERSION__["file"].relative_to(root)), Font.RecMono(), 24), ) .stack(20) .align(f.a.r) .rotate(f.e("l", 0, r=(0, 360)))) ================================================ FILE: examples/animations/versioned_with_sidecar_versions.py ================================================ from coldtype import * VERSIONS = {} for file in sorted(ººsiblingºº(".").glob("*.py")): if not file.stem.startswith("_"): VERSIONS[file.stem] = dict(file=file) ================================================ FILE: examples/animations/vertical_scale.py ================================================ from coldtype import * @animation(tl=50, bg=1) def scratch(f): ri = f.a.r.inset(30) rng = (1, 3) fs = 110 hello = (StSt("Hello", Font.MuSan(), fs, case="upper", wght=1, wdth=1, fit=ri.w) .align(ri, "N", ty=1, tx=0) .map(lambda i, p: p.scale(1, f.adj(i).e("eeio", rng=rng), pt=p.ambit(ty=1).pn))) there = (StSt("There", Font.MuSan(), fs+100, case="upper", wght=0.5, wdth=1, fit=ri.w) .align(ri, "C", ty=1, tx=0) .map(lambda i, p: p.scale(1, f.adj(i).e("eeio", rng=(3, 0.65)), pt=p.ambit(ty=1).pc))) world = (StSt("World", Font.MuSan(), fs, case="upper", wght=1, wdth=1, fit=ri.w) .align(ri, "S", ty=1, tx=0) .map(lambda i, p: p.scale(1, f.adj(i).e("eeio", rng=rng), pt=p.ambit(ty=1).ps))) return hello + there + world ================================================ FILE: examples/animations/warpblur.py ================================================ from coldtype import * from coldtype.fx.warping import warp from coldtype.fx.skia import phototype keyframes = [ dict(wdth=0, wght=0, rotate=-15, leading=200, font_size=700, warp=0, blur=15), dict(wdth=1, wght=1, rotate=0, leading=10, font_size=50, warp=200, blur=5), dict(wdth=0, wght=1, rotate=15, leading=100, font_size=320, warp=50, blur=3), dict(wdth=0.5, wght=0.5, rotate=0, leading=-470, font_size=250, warp=0, blur=1)] at = AsciiTimeline(8, 30, """ < 0 1 2 3 """, keyframes).shift("end", +10) @animation(timeline=at, bg=0) def warp_blur(f): state = f.t.kf("eeio") return (StSt("WARP\nBLUR", Font.MuSan(), ro=1, **state) .xalign(f.a.r) .align(f.a.r) .pen() .f(1) .ch(warp(None, # can be a number like 5 to preserve curves f.i*30, f.i, mult=int(state["warp"]))) .ch(phototype(f.a.r, state["blur"], cutw=50))) ================================================ FILE: examples/animations/wheee.py ================================================ from coldtype import * # keyboard shortcut backslash to clear visual buffer @animation((1080, 1080), timeline=120, bg=0, composites=1) def wheee(f): return P( f.last_render(lambda img: img.rotate(0)), (StSt("TYPE", Font.MuSan(), f.e("qeio", 1, r=(210, 70)) , wdth=f.e("qeio", r=(1, 0)) , wght=f.e("qeio")) .align(f.a.r) .rotate(f.e("l", 2, cyclic=0, r=(0, 360))) .fssw(hsl(f.e("l", 2, cyclic=0), s=0.6), 0, 4, 1))) ================================================ FILE: examples/apkjr.py ================================================ from coldtype import * from coldtype.raster import * fonts = [f for f in Font.List(r".*", r"_wood|_historical") if "Catchwords" not in f.name and "cmu" not in f.name and "AmericanStars" not in f.name and "BordersOne" not in f.name and "BookJacketAlts" not in f.name] @animation((1080, 1080/3), bg=0, tl=Timeline(60, 12)) def options(f): rsi = random_series(0, len(fonts), f.i, 10000, mod=int) p = P() txt = "HELLO" idx = 0 last = None total_width = 0 for c in txt: while True: #print(idx) fnt = fonts[rsi[idx]] if fnt == last: idx += 1 continue last = fnt idx += 1 try: candidate = StSt(c, fnt, 200)[0] idx += 1 if "notdef" in candidate.data("glyphName"): #print("! notdef") pass else: candidate.scaleToRect(f.a.r.take(200, "CY")).zero(tx=1, ty=1).unframe() w = candidate.ambit(tx=1).w if idx < 5000 and (total_width + w) > 900: #print("! too wide") continue total_width += w #print(">", c, candidate.data("glyphName"), fnt.name) p.append(candidate) break except: pass return (p #.mapv(lambda p: p.up().insert(0, P(p.ambit(tx=0, ty=0)).f(hsl(0.3, a=0.3))).zero()) #.mapv(λ.zero(tx=1, ty=1).unframe()) .spread(10, tx=1, zero=1) .align(f.a.r) .f(0) .f(1).ch(phototype(f.a.r, 2, 80, 30, 1)) ) @animation(tl=Timeline(60, 12), bg=0, solo=1) def three(f): return (P( options.pass_img(f.i).in_pen().ch(luma(f.a.r)).ch(precompose(f.a.r)).ch(fill(hsl(0.6, 0.7))), options.pass_img(f.i+10).align(f.a.r, "C").in_pen().ch(luma(f.a.r)).ch(precompose(f.a.r)).ch(fill(hsl(0.40, 0.65))), options.pass_img(f.i+20).align(f.a.r, "N").in_pen().ch(luma(f.a.r)).ch(precompose(f.a.r)).ch(fill(hsl(0.90, 0.7))) )) ================================================ FILE: examples/apng.py ================================================ from coldtype import * def release_apng(a:animation): fe = FFMPEGExport(a, False, loops=1) fe.fmt = "png" fe.args = fe.args[:-4] + ["-plays", "0", "-f", "apng"] fe.write(verbose=True) fe.open() @animation((540, 720/2), tl=Timeline(30, 30), release=release_apng) def apng(f:Frame): return (StSt(".PNG", Font.MuSan(), 200, wght=f.e("eeio")) .align(f.a.r, ty=0) .f(hsl(f.e("l", 0 ))) .rotate(f.e("eeio", 1, rng=(-20, 20)))) ================================================ FILE: examples/axidraw/hatching.py ================================================ from coldtype import * from coldtype.axidraw import * co = Font.ColdtypeObviously() script = Font.JBMono() @axidrawing(flatten=50) def test_draw(r): border = P(r.inset(50)).tag("border") letters = (StSt("COLD", co, 900, wdth=0.15, ro=1) .pen() .align(r) .tag("letters") .flatten(10)) hatch_rs = r.inset(20).subdivide(150, "N") hatches = (P().enumerate(hatch_rs, lambda x: P(x.el) if x.i%2==0 else None) .pen() .intersection(letters.copy()) .explode() .map(lambda _, p: P().line(p.ambit().es)) .s(0, 0.25) .tag("hatches")) typ = (StSt("type", script, 500 , ro=1 , tu=-120 , kp={"y.italic/p":-20}) .align(r, ty=1) .translate(0, -20) .s(hsl(0.65)) .tag("type")) return P(border, letters, typ, hatches) numpad = { 1: test_draw.draw("border"), 2: test_draw.draw("letters"), 3: test_draw.draw("type"), 4: test_draw.draw("hatches"), } ================================================ FILE: examples/axidraw/nextdraw.py ================================================ from coldtype import * from coldtype.runon.path import P from coldtype.renderable import renderable from coldtype.geometry import Rect, Point from time import sleep import time from fontTools.pens.basePen import BasePen from fontTools.pens.transformPen import TransformPen from coldtype.geometry import Rect, Point try: from nextdraw import NextDraw except: print("Couldn’t import nextdraw_api") print("https://bantam.tools/nd_py/#installation") print("uv pip install https://software-download.bantamtools.com/nd/api/nextdraw_api.zip") class NextDrawPen(BasePen): def __init__(self, dat, page, move_delay=0): super().__init__(None) self.dat = dat self.page = page self.ad = None self.move_delay = move_delay #dat.replay(self) self.last_moveTo = None def _moveTo(self, p): self.last_moveTo = p time.sleep(self.move_delay) self.ad.moveto(*p) time.sleep(self.move_delay) def _lineTo(self, p): time.sleep(self.move_delay) self.ad.lineto(*p) def _curveToOne(self, p1, p2, p3): print("! CANNOT CURVE !") def _qCurveToOne(self, p1, p2): print("! CANNOT CURVE !") def _closePath(self): # can this work? if self.last_moveTo: self.ad.lineto(*self.last_moveTo) def draw(self, scale=0.01, cm=False, ad=None, move_delay=0, zero=True, ): self.dat.scale(scale, point=Point(0, 0)) page = self.page.scale(scale) bounds = self.dat.bounds() limits = Rect(0, 0, 11, 8.5) if cm: limits = limits.scale(2.54) def small_enough(r): return (r.mnx >= 0 and r.mny >= 0 and r.mxx <= limits.w and r.mxy <= limits.h) if small_enough(page) and small_enough(bounds): print("Drawing!") else: print("Too big!", page, bounds) return False own_ad = False if not ad: own_ad = True ad = NextDraw() ad.interactive() ad.options.units = 1 if cm else 0 ad.options.speed_pendown = 50 ad.options.speed_penup = 50 ad.options.pen_rate_raise = 50 if own_ad: ad.connect() ad.penup() self.ad = ad self.move_delay = move_delay tp = TransformPen(self, (1, 0, 0, -1, 0, page.h)) self.dat.replay(tp) ad.penup() time.sleep(move_delay) ad.penup() if zero: ad.moveto(0,0) if own_ad: ad.disconnect() def aximeta(fn): def _aximeta(pen:P): pen.data(aximeta=dict(fn=fn)) return _aximeta def dip_pen(seconds=1, location=(0, 0)): return (P() .ch(aximeta(lambda ad: ad .moveto(*location) .pendown() .sleep(seconds) .penup() .moveto(0, 0)))) class NextDrawChainable(): def __init__(self, ad): self.ad = ad def moveto(self, x, y): self.ad.moveto(x, y) return self def penup(self): self.ad.penup() return self def pendown(self): self.ad.pendown() return self def sleep(self, t): sleep(t) return self class nextdrawing(renderable): def __init__(self, vertical=False, flatten=10, **kwargs ): self.flatten = flatten self.vertical = vertical if self.vertical: super().__init__(rect=(850, 1100), **kwargs) else: super().__init__(rect=(1100, 850), **kwargs) def runpost(self, result, render_pass, renderer_state, config): def normalize(p, pos, data): if pos != 0: return if self.flatten: p.flatten(self.flatten, segmentLines=False) s = p.s() if not s or (s and s.a == 0): p.fssw(-1, 0, 3) else: p.fssw(-1, s, 3) res = (super() .runpost(result, render_pass, renderer_state, config) .walk(normalize)) return res def draw(self, tag=None, flatten=None, frame=0, test=False, speed_pendown=100, speed_penup=100, pen_rate_raise=100, pen_rate_lower=100, pen_delay_down=0, move_delay=0, ): def _draw(_): ad = None def walker(p:P, pos, _): if pos == 0: ameta = p.data("aximeta") if ameta: fn = ameta.get("fn") if fn: fn(NextDrawChainable(ad)) return p = p.cond(flatten, lambda p: p.flatten( flatten, segmentLines=False)) ap = NextDrawPen(p, Rect(0, 0, 1100, 850)) ap.draw(ad=ad, move_delay=move_delay, zero=False) res = self.frame_result(frame, post=True) if self.vertical: res = res.copy().rotate(90, point=Point(0, 0)).translate(1100, 0) if tag is not None: if isinstance(tag, int): res = res[tag].copy(with_data=True) else: res = res.find_(tag).copy(with_data=True) if test: print("-"*30) print("NEXTDRAW TEST") print(">", res) print("-"*30) else: ad = NextDraw() ad.interactive() ad.options.units = 0 ad.options.speed_pendown = speed_pendown ad.options.speed_penup = speed_penup ad.options.pen_rate_raise = pen_rate_raise ad.options.pen_rate_lower = pen_rate_lower ad.options.pen_delay_down = pen_delay_down ad.connect() print("connected/") ad.penup() ad.moveto(0,0) try: res.walk(walker) except Exception as e: print(">>>", e) finally: ad.penup() ad.moveto(0,0) ad.disconnect() print("/disconnected") return _draw ########################## ### Actual custom code ### ########################## @nextdrawing(flatten=50) # <- tweak this value; lower for better precision def plot(r:Rect): return (P() .oval(r.inset(200).square()) .tag("circle")) numpad = { 1: plot.draw("circle") } ================================================ FILE: examples/axidraw/sheet.py ================================================ from coldtype import * from coldtype.axidraw import * @animation((120, 120), tl=(16**2, 24)) def sheet_animation(f): return (StSt("A", Font.MuSan(), 60, wght=f.e("eeio", 4)) .align(f.a.r, ty=1) .removeOverlap()) @axidrawing() def sheet(r): return sheet_animation.contactsheet(r.inset(20), 0.75) numpad = { 1: sheet.draw("border"), 2: sheet.draw("grid"), 3: sheet.draw("frames"), } ================================================ FILE: examples/axidraw/sheet_read.py ================================================ from coldtype import * from coldtype.img.skiaimage import SkiaImage from coldtype.fx.skia import phototype, precompose # res = ["media/DSC01538.JPG", 8, -0.45, -384, -253, 327, 233, 0, 0] # res = ["media/DSC01539.JPG", 7, -0.45, -1084, -453, 337, 233, -9, 4] res = ["/Users/robstenson/Sites/othermodern.com/media/_goodhertz/hires/Capture One Catalog01461RAW lossless compressed.jpg", 8, -0.45, -954, -333, 327, 233, 0, -9, 4] #res = ["media/DSC01796.JPG", 8, -0.45, -1054, -443, 298, 204, 0, -6, 10] #res = ["media/DSC02053.JPG", 8, -0.45, -444, -143, 408, 284, 0, -20, -10] #res = ["media/DSC02309.JPG", 8, -0.5, -354, -33, 443, 304, 0, -20, 20] #res = ["media/DSC02565.JPG", 8, 0, -327, -80, 435, 302, -40, -10, 37] #res = ["media/DSC02821.JPG", 8, -0.5, -327, -180, 378, 258, 0, 0, 0] #res = ["media/DSC03077.JPG", 8, -0.75, -688, -448, 318, 219, 0, 0, 0] #res = ["/Users/robstenson/Pictures/SigmaFPLTetherTest1/Output/SigmaFPLTetherTest10002 1 2.jpg", 8, 0, -684, -133, 912, 703, 0, -9, 4] imgp, sq, ro, xo, yo, xa, ya, xra, xf, yf = res @animation((1080, 1080), tl=Timeline(16*16, 18), bg=1) def sheet_read(f): img = SkiaImage(ººsiblingºº(imgp)) x = f.i%16 y = f.i//16 xe = x/sq ye = y/sq return (P( img .t(-271, -5080) .t(x*-468, y*357) #.in_pen(), #.rotate(ro, point=Point(0,0)) #.t(xo-(x*xa)+xe*xf+ye*xra, yo-(y*ya)+xe*yf), #StSt("A", Font.MuSan(), 447, wdth=0).align(f.a.r) #P(f.a.r.take(60, "C")).outline() ) #.layer(1, lambda _: P(f.a.r).f(1).blendmode(BlendMode.Difference)) #.ch(precompose(f.a.r)) #.ch(phototype(f.a.r, blur=0, cut=172, cutw=26, fill=1)) ) ================================================ FILE: examples/bg_fn.py ================================================ from coldtype import * tl = Timeline(30, 12) @animation((540, 540), tl=tl, render_only=1) def bg_maker(f): return P( P(f.a.r).f(hsl(f.e("l", 0))), StSt(f"{f.i}", Font.JBMono(), 250, wght=1, wdth=0) .f(1) .align(f.a.r, tx=0)) @animation(bg_maker.rect, bg=lambda _,rp: bg_maker.pass_img(rp.idx).rotate(-rp.idx), render_bg=1, tl=tl) def bg_user(f): return P(f.a.r.inset(40)).fssw(-1, 1, 6) ================================================ FILE: examples/bg_img.py ================================================ from coldtype import * from coldtype.fx.skia import phototype r = Rect(1080, 540) @renderable(r) def bg(r): return P( P(r).f(Gradient.Vertical(r, hsl(0.3), hsl(0.6))), StSt("BG", Font.MuSan(), 250, wght=1, wdth=0) .f(1) .align(r) .ch(phototype(r, blur=3, cutw=30))) @renderable(r, bg=bg) def bg_user(r): return P(r.inset(50)).fssw(-1, 1, 6) ================================================ FILE: examples/blender/arch.py ================================================ from coldtype import * from coldtype.blender import * """ An arch, dynamically generated from a bezier curve; then physics are enabled and the whole thing falls down """ tl = Timeline(90, fps=30) @b3d_runnable() def setup(bpw:BpyWorld): BpyObj.Find("Cube").delete() BpyObj.Find("Light").locate(y=-5) (bpw.delete_previous(materials=False) .cycles(32, False, Rect(1080, 1080)) .timeline(tl, output=setup.output_folder / "arch1_") .rigidbody(2.5, 300)) (BpyObj.Cube("Floor") .scale(x=100, y=100, z=0.2) .locate(z=1.65) .apply_scale() .rigidbody("passive", friction=1, bounce=0) .material("floor-material", lambda m: m .f(bw(0)))) @b3d_renderable(reset_to_zero=1, upright=1, center=(0, 1)) def arch(r): r = r.inset(200) curve:P = (P() .moveTo(r.psw) .boxCurveTo(r.pn, "NW", factor:=.65) .boxCurveTo(r.pse, "NE", factor) .fssw(-1, 0, 2)) return (P().enumerate(curve.samples(20), lambda x: P() .declare( q:=0.49, start:=x.el.pt.project(x.el.tan, d:=100), end:=x.el.pt.project(x.el.tan, -d)) .moveTo(start.interp(q, x.el.next.pt.project(x.el.next.tan, d))) .lineTo(start.interp(q, x.el.prev.pt.project(x.el.prev.tan, d))) .lineTo(end.interp(q, x.el.prev.pt.project(x.el.prev.tan, -d))) .lineTo(end.interp(q, x.el.next.pt.project(x.el.next.tan, -d))) .closePath() .f(0) .tag(f"sample_{x.i}") .ch(b3d(lambda bp: bp .extrude(1.0) .convert_to_mesh() .rigidbody(friction=1, bounce=0) , upright=1 , zero=1)))) ================================================ FILE: examples/blender/array_separate.py ================================================ from coldtype import * from coldtype.blender import * @b3d_runnable() def setup(bpw:BpyWorld): bpw.delete_previous() (BpyObj.Cube("Cube") .dimensions(x=0.5, y=0.5, z=0.5) .arrayX(10, constant=1, relative=None) .apply_modifier("Array") .arrayZ(10, constant=1, relative=None) .apply_modifier("Array") .separate_by_loose_parts() .map(lambda bp: bp.origin_to_geometry())) ================================================ FILE: examples/blender/bauhaus_book_14.py ================================================ from coldtype import * from coldtype.blender import * from coldtype.fx.skia import phototype txt = [ "bauhausbücher", "moholy-nagy", "von\nmaterial\nzu\narchitektur", ] r = Rect("letter").scale(2) @renderable(r, bg=hsl(0), render_bg=0) def cover(r): s = Scaffold(r.inset(100*2, 110*2)) s1 = Style("GrossV", 60*2, wdth=1, wght=0.75) txts = (P( StSt(txt[0], s1).f(1) .align(s, "NE"), StSt(txt[1], s1.mod(fontSize=51*2)).f(0) .align(s, "W") .t(0, 10), StSt(txt[2], s1.mod(fontSize=82*2)).f(0) .align(s, "SW"))) number = (P(Rect(87*2, 984)) .layer( lambda p: p.layer( 1, lambda p: p.copy() .scale(1, 1) .skew(0.45, 0) .align(p.ambit(), "NE") .drop(130*2, "S"), lambda p: p.copy() .take(p.w, "S") .scale(2.75, 1) .t(-40*2, 130*2)), lambda p: p.layer( 1, lambda p: p.copy() .scale(1.1, 1) .skew(0.60, 0) .align(p.ambit(), "NE") .take(p.w*1.85, "E"))) .map(lambda p: p.pen(frame=False).ro()) .reverse() .spread(-67*2, zero=True) .xor() .f(0) .print() .layer(1, lambda p: P().enumerate(p.ambit().subdivide_with_leading(60, 14, "N"), lambda x: P(x.el.o(0, 1))).pen()) .intersection() .f(1) .align(s.r, "NE") .t(0, 77) #.f(0) .ch(phototype(r, 0.5, 130, 30, fill=bw(0))) ) return P(number, txts) @b3d_runnable(force_refresh=1) def setup(blw:BpyWorld): blw.delete_previous(materials=False) (BpyObj.Plane("GlassPlate") .scale(4, r.aspect()*4) .rotate(x=85) .solidify(0.0005) .locate_relative(y=0.001) .material("glass_plate_material")) (BpyObj.Plane("Glass") .scale(4, r.aspect()*4) .rotate(x=85) .material("glass_material", lambda m: m .f(0) .specular(0) .image(cover.pass_path(0)))) ================================================ FILE: examples/blender/boston.py ================================================ from coldtype import * from coldtype.blender import BlenderTimeline, b3d_sequencer # sound: https://accent.gmu.edu/browse_language.php?function=detail&speakerid=79 # typed with: https://westonruter.github.io/ipa-chart/keyboard/ """ pʰliz kalˠ stɛlɘ æsk hɚ ɾɘ bɹɪŋ ðiːz θɪŋɡz wɪð hɚ fɹɔm nɘ stɔɘ sɪks spuːnz ʌv fɹɛʃ snoʊ pʰiːz faɪv tɪk slæbz ʌv blu tʃiːz n meɪbi ɘ snæk fɔɹ hɚ bɹʌðɘ bɔɘb """ bt = BlenderTimeline(ººBLENDERºº, 275) print(bt.file) @b3d_sequencer((1080, 1080) , timeline=bt , bg=hsl(0.5) , live_preview_scale=0.5 , audio=ººsiblingºº("media/pleasecallstella.wav") ) def lyrics(f:Frame): return (f.t.words.currentWord() .pens(f.i, lambda c: (c.text, Style("Brill Italic", 350, tu=30))) .removeFutures() .align(f.a.r) .f(1) .insert(0, lambda p: P(p.ambit().inset(-20).drop(40, "N")).f(0))) ================================================ FILE: examples/blender/direct_objects.py ================================================ from coldtype import * from coldtype.blender import * """ Here there is no simultaneous 2D/3D; we're just using Coldtype to script Blender directly (i.e. this script only works when Blender is running) """ @b3d_runnable() def setup(blw:BpyWorld): (blw.delete_previous("Coldtype" , materials=True) .delete_previous("Cubes" , materials=True) .timeline(Timeline(120) , resetFrame=0 , output=setup.output_folder / "do1_" , version=1)) blw.rigidbody(1.5, 120) (BpyObj.Empty("Empty1")) (BpyObj.Curve("ObviGlyph") .draw(StSt("TYPE", Font.ColdObvi(), 5) .centerZero()) .extrude(0.2) .locate(z=7.5) .rotate(x=90) .convert_to_mesh() .rigidbody("active", bounce=0.5) .material("coldtype_material") .shade_flat()) (BpyGroup.Curves( StSt("COLD", Font.MuSan(), 3 , wght=1, wdth=1) .centerZero() , collection="/Glyphs") .map(lambda bp: bp .parent("Empty1") .extrude(0.25) .locate(z=15) .convertToMesh() .rigidbody("active", bounce=0.5) .material("coldtype_material"))) BpyObj.Find("Empty1").locate(z=-2) (BpyMaterial.Find("monkey_material") .f(hsl(0.3, 1)) .roughness(1) .specular(0)) monkey = (BpyObj.Monkey() .locate(z=11) .rotate(z=45) .scale(1,1,1) .rigidbody("active", bounce=0.3) .material("monkey_material") .subsurface() .shade_smooth()) monkey.copy().locate(x=6) monkey.copy().locate(x=-6) (BpyMaterial.Find("monkey_material") .f(hsl(0.6, 1))) (BpyObj.UVSphere("Cube1", collection="Cubes") .scale(2, 2, 2) .locate(z=20) .rigidbody("active", bounce=0.3) .material("cube_material", lambda m: m .f(hsl(0.85, 1)) .transmission(1)) .subsurface() .shade_smooth()) (BpyObj.Plane() .scale(x=30, y=30) .apply_scale() .rigidbody("passive", bounce=0.5) .material("plane_material", lambda m: m .f(hsl(0.17, 0.8, 0.5)) .specular(0))) ================================================ FILE: examples/blender/displace.py ================================================ from coldtype import * from coldtype.blender import * """ A giant letter, w/ the top face displaced with a Displace modifier; then animated by moving the Displace modifier's coords object w/ a @b3d_animation """ big_c = "C" @b3d_runnable() def setup(bw:BpyWorld): (bw.deletePrevious(materials=True)) glyph = (StSt(big_c, Font.ColdObvi(), 8.5, wght=1) .pen() .shift(-0.5, 0.5)) (BpyObj.Curve("Glyph") .draw(glyph) .extrude(1) .origin_to_cursor() .rotate(x=90) .convert_to_mesh() .remesh(6) .apply_modifier("Remesh") .make_vertex_group(lambda p: p.co[2] > 0, name="front") .add_empty_origin() .displace( strength=3.35, midlevel=0, texture="Texture", coords_object="Glyph_EmptyOrigin", direction="Z", vertex_group="front") .subsurface() .smooth(factor=6, repeat=2, x=0, y=0, z=1) .shade_smooth() .material("sponge", lambda m: m .f(hsl(0.65)))) @b3d_animation(tl=30, name=f"animator_{big_c}") def animator(f): if bpy and bpy.data: (BpyObj.Find("Glyph_EmptyOrigin") .locate(x=ord(big_c)+f.e("l", 0, r=(0, 2))) .rotate(x=f.e("l", 0, r=(0, 5)))) @b3d_runnable(delay=True) def post_setup(bw:BpyWorld): bw.scene.frame_set(20) ================================================ FILE: examples/blender/dof.py ================================================ from coldtype import * from coldtype.blender import * """ A variable font animation that works in 2D and 3D; also a little bit of camera manipulation in here to dynamically adjust depth-of-field in coordination with the letters’ movement """ rs1 = random_series(-5, 5) @b3d_animation(timeline=180, upright=1) def var3d2(f): if bpy and bpy.data: bpy.data.cameras["Camera"].dof.aperture_fstop = f.e("ceio", 1, rng=(0.1, 0.01)) return (StSt("DEPTH\nOF\nFIELD", Font.MutatorSans() , fontSize=f.e("eeio", 2, rng=(150, 100)) , wdth=f.e("eeio", 1) , wght=f.e("ceio", 4) , leading=30 , ro=1) .xalign(f.a.r) .align(f.a.r) .collapse() .mapv(lambda i, p: p .tag(f"glyph_{i}") .ch(b3d(lambda bp: bp .extrude(0.1) .locate( x=0, y=rs1[i]*f.e("eeio", 2, rng=(0, 1.5)) if i != 5 else 0, z=0))))) ================================================ FILE: examples/blender/dominos.py ================================================ from coldtype import * from coldtype.blender import * frames = 160 suffix = "falling_" text = """FALLING DOWN""" @b3d_runnable(playback=B3DPlayback.KeepPlaying) def setup(bpw:BpyWorld): (bpw.delete_previous(materials=False) .timeline(Timeline(frames, 30), resetFrame=0 , output=setup.output_folder / suffix) .rigidbody(2.5, frames)) (BpyObj.Cube("Floor") .dimensions(x=30, y=30, z=1) .locate(z=-0.5) .rigidbody("passive") .material("floor_mat")) r = Rect(10) curves = P( P().line([r.pw, r.pe]).endPath().centerZero().t(0, 0), P().line([r.pw, r.pe]).endPath().centerZero().t(0, -4), #P().line([r.pw, r.pe]).endPath().centerZero().t(0, -4*2), ).t(-1.5, 2.5) dominos = [] for idx, curve in enumerate(curves): points = curve.samples(1.3) txt = text.split("\n")[idx] for pt in points: try: glyph = StSt(txt[pt.idx], Font.MuSan(), 4, wdth=1, wght=0).pen() glyph.t(-glyph.ambit(tx=1).x, -glyph.ambit(ty=1).y) except IndexError: glyph = None if glyph: dominos.append(BpyObj.Curve(f"Letter_{pt.idx}") .draw(glyph) .extrude(0.15) .with_temp_origin((0,0,0), lambda bp: bp.rotate(y=-90)) .convert_to_mesh() .apply_transform() .locate(x=pt.pt[0], y=pt.pt[1]) .material("letter_mat")) pt = points[0] a = pt.pt.o(0, 1.25).project(pt.tan-90, -0.5) b = pt.pt.o(0, 1.25).project(pt.tan-90, 0.5) c = a.project(pt.tan-90, -3) czh = 3 (BpyObj.Cube("Catalyst") .dimensions(0.25, 2, cz:=0.5) .locate(x=a.x, y=a.y, z=czh) # .origin_to_cursor() # .rotate(z=pt.tan+180) # .apply_transform() .rigidbody("passive", animated=True, friction=1) .hide() .insert_keyframes("location", (0+idx*(n:=50), lambda bp: bp.locate(x=a.x, y=a.y, z=czh)), (10+idx*n, lambda bp: bp.locate(x=b.x, y=b.y, z=czh)), (20+idx*n, lambda bp: bp.locate(x=c.x, y=c.y, z=czh))) # #.origin_to_geometry() .set_frame(0) ) dm:BpyObj for dm in dominos: (dm.apply_transform() .origin_to_geometry() .rigidbody(mass=10, friction=0.35, deactivated=True)) ================================================ FILE: examples/blender/dominos2.py ================================================ from coldtype import * from coldtype.blender import * samples, x, y, z, frames = [ (1.5, 3, 0.5, 5, 120), (0.5, 3, 0.25, 5, 120), (0.25, 3, 0.15, 5, 180), (1.2, 3, 0.25, 5, 90) ][3] letter = "A" suffix = f"alphabet_{samples}_{x}_{y}_{z}__" @b3d_runnable(playback=B3DPlayback.KeepPlaying) def setup(bpw:BpyWorld): (bpw.delete_previous(materials=False) .timeline(Timeline(frames, 30), resetFrame=0 , output=setup.output_folder / suffix) .rigidbody(2.5, 250)) (BpyObj.Cube("Floor") .dimensions(x=30, y=30, z=1) .locate(z=-0.5) .rigidbody("passive") .material("floor_mat")) r = Rect(10) curve = P().oval(Rect(10)).centerZero().repeat(1).subsegment(0, 0.505) curve = P().line([r.pw, r.pe]).endPath().centerZero().t(0, -1) points = curve.samples(samples) dominos = [] text = "MONUMENTAL" for pt in points: if False: dominos.append(BpyObj.Cube(f"Cube_{pt.idx}") .dimensions(x, y, z) .locate(z=z/2) .rotate(z=pt.tan) .locate(x=pt.pt[0], y=pt.pt[1]) .material("letter_mat")) else: try: glyph = StSt(text[pt.idx], Font.MuSan(), 5, wdth=1, wght=0.25, opsz=1).pen() glyph.t(-glyph.ambit(tx=1).x, -glyph.ambit(ty=1).y) except IndexError: glyph = None if glyph: dominos.append(BpyObj.Curve(f"Letter_{pt.idx}") .draw(glyph) .extrude(0.35) .with_temp_origin((0,0,0), lambda bp: bp.rotate(y=-90)) .convert_to_mesh() .apply_transform() #.rotate(z=pt.tan+180) #.rotate(z=pt.tan+180) .locate(x=pt.pt[0], y=pt.pt[1]) #.convert_to_mesh() #.apply_transform() .material("letter_mat") ) pt = points[0] a = pt.pt.project(pt.tan, 0).project(pt.tan-90, -0.5) b = pt.pt.project(pt.tan, 0).project(pt.tan-90, 1.5) (BpyObj.Cube("Catalyst") .dimensions(xx:=2, 0.65, zz:=1.75) .locate(x=-1, y=0, z=zz) .origin_to_cursor() .rotate(z=pt.tan+180) .apply_transform() .rigidbody("passive", animated=True, friction=1) #.hide() .insert_keyframes("location", (0, lambda bp: bp.locate(x=a.x, y=a.y, z=zz/2)), (20, lambda bp: bp.locate(x=b.x, y=b.y, z=zz/2)), (50, lambda bp: bp.locate(x=a.x, y=a.y, z=zz/2))) #.origin_to_geometry() ) for dm in dominos: (dm.apply_transform() .origin_to_geometry() .rigidbody(mass=10, friction=0.35, deactivated=True)) ================================================ FILE: examples/blender/dominos3.py ================================================ from coldtype import * from coldtype.blender import * samples, x, y, z, frames = [ (1.5, 3, 0.5, 5, 120), (0.5, 3, 0.25, 5, 120), (0.25, 3, 0.15, 3, 120), (1.2, 3, 0.25, 10, 120) ][2] letter = "A" suffix = f"alphabet_capZ_{samples}_{x}_{y}_{z}__" rs1 = random_series(seed=2) @b3d_runnable(playback=B3DPlayback.KeepPlaying) def setup(bpw:BpyWorld): (bpw.delete_previous(materials=False) .timeline(Timeline(frames, 30), resetFrame=0 , output=setup.output_folder / suffix) .rigidbody(2.5, frames)) (BpyObj.Cube("Floor") .dimensions(x=30, y=30, z=1) .locate(z=-0.5) .rigidbody("passive") .material("floor_mat")) curve = P().oval(Rect(10)).centerZero().repeat(1).subsegment(0, 0.505) points = curve.samples(samples) dominos = [] for idx, pt in enumerate(points[:-2]): if rs1[idx] <= 1: #> 0.85: dominos.append( #BpyObj.Cube(f"Cube_{pt.idx}") #.dimensions(x, y, z) BpyObj.Curve(f"Cube_{pt.idx}") .draw(StSt("A", "Lang", 4, wdth=1).pen()) .rotate(x=90) .extrude(0.05) .convert_to_mesh() .locate(z=z/2) .rotate(z=pt.tan) .locate(x=pt.pt[0], y=pt.pt[1]) .material("letter_mat")) pt = points[0] a = pt.pt.project(pt.tan, -2).project(pt.tan-90, -0.5) b = pt.pt.project(pt.tan, -2).project(pt.tan-90, 0.5) c = pt.pt.project(pt.tan, -10) (BpyObj.Cube("Catalyst") .dimensions(2, 0.65, zz:=1.75) .locate(x=-1, y=0, z=zz) .origin_to_cursor() .rotate(z=pt.tan+180) .apply_transform() .rigidbody("passive", animated=True, friction=1) .hide() .insert_keyframes("location", (0, lambda bp: bp.locate(x=a.x, y=a.y, z=zz/2)), (20, lambda bp: bp.locate(x=b.x, y=b.y, z=zz/2)), (50, lambda bp: bp.locate(x=c.x, y=c.y, z=zz/2)))) for dm in dominos: (dm.apply_transform() .origin_to_geometry() .rigidbody(mass=10, friction=0.35, deactivated=True)) ================================================ FILE: examples/blender/hobeauxborders.py ================================================ from coldtype import * from coldtype.blender import * """ If you have a copy of Hobeaux Rococeaux Borders (https://ohnotype.co/fonts/hobeaux-rococeaux), you can make lovely frames using this code """ fnt = Font.Find("Hobeaux-Roc.*Bor") styles = [ "Aa ", "B ", "Cc3", "Dd4", "Ee5", "Ff6", "Gg ", "Hh8", "Ii9", "Jj0", "Kk!", "Ll@", "Mm#", "Nn$", "Oo%", "Pp^", "Qq&", "Rr ", "Ss(", "Tt)", "Uu-", "Vv=", "Ww_", "Xx+", "Yy ", ] def hobeauxBorder(r, style=0, fs=200): s = styles[style] b, c, m = [StSt(x, fnt, fs).pen() for x in s] bw, cw = [e.ambit().w for e in (b, c)] nh, nv = int(r.w/bw/2), int(r.h/bw/2) bx = Rect(bw*nh*2+cw*2, bw*nv*2).align(r) return (b.layer( lambda p: p .layer(nh) .append(c) .spread() .append(m) .mirrorx() .translate(*bx.pn) .mirrory(bx.pc), lambda p: p .layer(nv) .spread() .append(m.copy()) .rotate(90, point=(0, 0)) .translate(cw, 0) .mirrory() .translate(*bx.pw) .mirrorx(bx.pc)) .pen() .unframe()) @b3d_runnable() def setup(bpw:BpyWorld): bpw.delete_previous() @b3d_animation(timeline=len(styles)) def b1(f): return (hobeauxBorder(f.a.r.inset(150), f.i, 500) .scale(0.9) .tag("Pattern") .ch(b3d(lambda bp: bp .extrude(0.25) .material("Pattern_mat", lambda m: m .f(hsl(0.17, 0.8, 0.7)))))) ================================================ FILE: examples/blender/ifg.py ================================================ from coldtype import * from coldtype.blender import BlenderTimeline, b3d_sequencer from coldtype.fx.skia import phototype obv = Font.Find("ObviouslyV", "__variables") bt = BlenderTimeline(ººBLENDERºº, 5400) ifg = bt.interpretWords(include="+1") action = bt.interpretWords(include="+2") adlib = bt.interpretWords(include="+4") suggestion = bt.interpretWords(include="+5") @b3d_sequencer((1920, 1080) , timeline=bt , bg=None , render_bg=False , live_preview_scale=0.25 ) def lyrics(f:Frame): r_ifg = f.a.r.inset(650, 446) def obvi(c): txt = c.text return (txt.upper(), Style(obv, 120, wdth=0.5, wght=0.85, ss01=1, slnt=1, tu=-20)) top = (ifg.currentGroup(f.i) .pens(f.i, obvi, fit=r_ifg.w) .align(r_ifg, "N") .removeFutures() .fssw(1, 0, 10, 1)) bottom = (action.currentGroup(f.i) .pens(f.i, obvi, fit=r_ifg.w) .align(r_ifg, "S") .removeFutures() .fssw(1, 0, 10, 1)) try: if "a song" in bottom[0][0][0].data("clip").text: bottom[0][0][0].filter(lambda idx, p: idx > 3) if "a bike" in bottom[0][0][0].data("clip").text: bottom[0][0][0].filter(lambda idx, p: idx > 3) if "a juice" in bottom[0][0][0].data("clip").text: bottom[0][0][0].filter(lambda idx, p: idx > 4) if "a cat" in bottom[0][0][0].data("clip").text: bottom[0][0][0].filter(lambda idx, p: idx > 2) except Exception as _: pass lockup = P(top, bottom)#.t(0, -220) ad = (adlib.currentGroup(f.i) .pens(f.i, lambda x: (x.text, Style("Lovesong", 100))) #.scale(0.5, 1) .align(f.a.r.drop(600, "S"), "CX") .rotate(15) .removeFutures() .pen() .fssw(1, 0, 10, 1)) sugg = (suggestion.currentGroup(f.i) .pens(f.i, lambda x: (x.text, Style("Softie", 200, wght=0.25))) #.scale(0.5, 1) .align(f.a.r.inset(250).drop(340, "S"), "N") .rotate(-15) .removeFutures() .pen() .fssw(1, 0, 10, 1)) style = bt.current(3) if style.name: if style.name == "scramble": lockup[0].mapv(lambda p: p.rotate(35)) lockup[1].mapv(lambda p: p.rotate(-15)) elif style.name == "scramble3": lockup[0].mapv(lambda p: p.rotate(-15)) lockup[1].mapv(lambda p: p.rotate(35)) elif style.name == "scramble2": lockup[0].mapv(lambda p: p.rotate(-15)) lockup[1].mapv(lambda p: p.rotate(35)) lockup.rotate(-15) ad.rotate(-20) if style.name == "fade": o = style.e("l", 0, rng=(1, 0)) lockup.alpha(o) ad.alpha(o) return P(P(lockup + ad).t(0, -220) + sugg).ch(phototype(f.a.r, 2, 150, 30)) ================================================ FILE: examples/blender/img.py ================================================ from coldtype import * from coldtype.blender import * from coldtype.fx.skia import rasterize """ Demonstration of two ways to display an image on a plane in Blender """ img_path = __sibling__("media/rasterized_test.png") img = (StSt("COLD\nTYPE", Font.ColdtypeObviously() , fontSize=500 , wdth=0.25) .align(Rect(1080, 1080)) .mapv(lambda p: p .f(Gradient.V(p.ambit(), hsl(0.3), hsl(0.7)))) .ch(rasterize(Rect(1080, 1080), img_path))) @b3d_runnable() def show_img_direct(bpw:BpyWorld): bpw.delete_previous() (BpyObj.Plane("DirectImage") .locate(z=0.25) .rotate(z=15) .scale(2, 2, 1) .material("direct_image_mat", lambda m: m .image(img_path))) @b3d_renderable() def show_img(r): return (P(r).f(-1).img(img_path, r) .ch(b3d(lambda p: p, material="auto", primitive="plane"))) ================================================ FILE: examples/blender/liveimage.py ================================================ from coldtype import * from coldtype.blender import * """ Demonstration of how to display a dynamic image on a plane """ chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" @renderable((1080, 1080), bg=0, render_bg=0) def embedded_image(r): char = "A" if bpy: char = chars[bpy.context.scene.frame_current] return (StSt(char, Font.MutatorSans(), 1000, ro=1, wdth=0, wght=1) .align(r) .pen() .f(1)) @b3d_runnable() def setup(bpw:BpyWorld): (bpw.delete_previous() .cycles(128)) @b3d_animation(tl=len(chars), force_refresh=True) def anim1(f): proj = BpyObj.Find("Reprojection") if not proj.obj: proj = BpyObj.Plane("Reprojection").scale(2.5, 2.5) (proj.material("embedded_image", lambda m: m .image(embedded_image, emission=0, render=True)) .rotate(x=-55)) ================================================ FILE: examples/blender/noordzijcube.py ================================================ from coldtype import * from coldtype.blender import * """ A classic 5-by-5 Noordzij Cube, displaying any variable font with three axes (https://letterror.com/articles/noordzij-cube.html) """ fnt = Font.Find("ObviouslyV") d = 5 @b3d_runnable() def setup(bpw:BpyWorld): (bpw.delete_previous() .cycles(128) .timeline(Timeline(240) , resetFrame=0 , output=setup.output_folder / "noord1_")) (BpyObj.Cube("Floor") .dimensions(x=d*2, y=d*2, z=0.25) .locate(x=d, y=d) .origin_to_cursor() .locate(x=-d/2, y=-d/2, z=-1) .material("floor_mat", lambda m: m .f(0) .specular(0) .roughness(1))) pivot = (BpyObj.Empty("Center") .locate(x=(d-1)/2, y=(d-1)/2, z=0) .insert_keyframes("rotation_euler", (0, lambda bp: bp.rotate()), (240, lambda bp: bp.rotate(z=360))) .make_keyframes_linear("rotation_euler")) (BpyObj.Find("Camera").parent(pivot)) def add_glyph(x, y, z): (BpyObj.Curve(f"Glyph_{x}_{y}_{z}") .draw(StSt("A", fnt, 0.5 , slnt=x/(d-1) , wdth=(y/(d-1)) , wght=1-((z/(d-1)))) .centerZero() .pen()) .rotate(x=90) .locate(x=x, y=y, z=z) .extrude(0.1) .convert_to_mesh() .material(f"letter_mat_{y}", lambda m: m .f(1) #.f(hsl(y/(d+1), 1, 0.8)) .specular(0) .roughness(1))) for z in range(0, d): for y in range(0, d): for x in range(0, d): add_glyph(x, y, z) ================================================ FILE: examples/blender/parched.py ================================================ from coldtype import * from coldtype.blender import * @animation((1080, 1080), timeline=360, solo=1) def varfont_animation_overlay(f): ease_curve = P().withRect(1000, lambda r, p: p .moveTo(r.psw) .ioEaseCurveTo(r.pne, 19, 10)) return (P( Glyphwise("PARCH", lambda g: Style("ParchVF", 50, DCAY=f.adj(g.i*10).e(ease_curve, 2, rng=(0.5, 0)))) .align(f.a.r.inset(50), "NW") .f(0), Glyphwise("OVERLAP TYPE", lambda g: Style("ParchVF", 30, DCAY=f.adj(g.i*10).e(ease_curve, 2, rng=(0.5, 1)))) .align(f.a.r.inset(50), "SE") .f(0), #ease_curve.copy().scaleToRect(f.a.r.inset(20)).align(f.a.r).fssw(-1, 0, 7) )) @animation((1080, 1080), timeline=360) def varfont_animation(f): ease_curve = P().withRect(1000, lambda r, p: p .moveTo(r.psw) .ioEaseCurveTo(r.pne, 19, 10)) return (P( Glyphwise("DRY\nCLEANERS", lambda g: Style("ParchVF", 230, DCAY=f.e(ease_curve, 0, rng=(0.5, 0 if g.l > 0 else 1)))) .xalign(f.a.r) .lead(100) .align(f.a.r) .align(f.a.r, tx=0) .f(0), #ease_curve.copy().scaleToRect(f.a.r.inset(20)).align(f.a.r).fssw(-1, 0, 7) )) @b3d_runnable(force_refresh=1) def setup(blw:BpyWorld): (blw .delete_previous(materials=False) .timeline(Timeline(360, 24), output=setup.output_folder / "p1_")) sun = BpyObj.Find("Light") (sun.insert_keyframes("rotation_euler", (0, lambda bp: bp.rotate(z=-170)), (360, lambda bp: bp.rotate(z=-44))) #.make_keyframes_linear("rotation_euler") ) (BpyObj.Plane("Projection") .scale(3, 3) .rotate(x=90) .locate(0, 0, 2) .material("projection_material", lambda m: m .f(0) .specular(0) .animation(varfont_animation))) ================================================ FILE: examples/blender/physics_direct.py ================================================ from coldtype import * from coldtype.blender import * """ Some text falls from on high (using Blender directly via @b3d_runnable) """ txt = "FALL\nING\nTEXT" @b3d_runnable() def setup(bpw:BpyWorld): (bpw.delete_previous() .timeline(Timeline(90), resetFrame=0 , output=setup.output_folder / "ft1_") .rigidbody(speed=3, frame_end=1000)) (BpyObj.Find("Plane") .rigidbody("passive", friction=1, bounce=0) .material("floor_mat1", lambda m: m .f(1) .specular(1) .roughness(0.25))) (BpyGroup.Curves( StSt(txt, Font.MutatorSans(), 3 , wght=1 , leading=1) .map(lambda p: p.track_to_width(9)) .deblank() .centerZero()) .map(lambda bp: bp .extrude(0.275) .locate(z=30) .convert_to_mesh() .rigidbody(friction=0.5) .material("letter_mat1", lambda m: m .f(hsl(0.07, 1, 0.5)) .roughness(1) .specular(0)))) ================================================ FILE: examples/blender/physics_upright.py ================================================ from coldtype import * from coldtype.blender import * """ A 3D Physics simulation that uses lo-res polygons to do the actual physics, then uses copied, hi-res bezier curves (parented to the lo-res polygons) to display """ txt = "COLD\nTYPE" @b3d_runnable() def setup(bpw:BpyWorld): (bpw.delete_previous() .timeline(Timeline(150), resetFrame=0 , output=setup.output_folder / "ct1_") .cycles(128) .rigidbody(speed=2, frame_end=150)) (BpyObj.Plane("Field") .rigidbody("passive", friction=1, bounce=0) .dimensions(x=160, y=130) .material("field_mat", lambda m: m .f(0) .specular(0) .roughness(1))) lockup = (StSt(txt, Font.ColdObvi(), 7, wdth=0.5, leading=1) .map(lambda p: p.track_to_width(13)) .centerZero() .translate(0, 6) .deblank() .collapse()) lores = (BpyGroup.Curves(lockup, "Letter_Lores") .map(lambda idx, bp: bp .extrude(0.5) .with_temp_origin((0, 0, 0), lambda bp: bp .rotate(x=90)))) hires = lores.copy("Letter_Hires") lores.map(lambda bp: bp .convert_to_mesh() .remesh(3) .apply_modifier("Remesh") .rigidbody(friction=1, bounce=0.5)) hires.map(lambda idx, bp: bp .parent(f"Letter_Lores_{idx}", hide=True) .material("letter_mat", lambda m: m .f(1))) ================================================ FILE: examples/blender/reprojection.py ================================================ from coldtype import * from coldtype.blender import * """ a 2d coldtype variable font animation; in the same file as @b3d_runnable that displays that 2d animation in a 3d world """ # to fully re-cache Rendered sequence in blender, make sure to set force_refresh=1 on your @b3d_runnable @animation((540, 540), timeline=30) def varfont_animation(f): return (P( Glyphwise("COLD", lambda g: Style(Font.ColdObvi(), 250 , wdth=f.adj(-g.i*40).e("seio"))) .align(f.a.r, tx=0) .f(hsl(0.3)), StSt(str(f.i) , Font.JBMono(), 50) .align(f.a.r.inset(50), tx=0, y="S") .f(0))) @b3d_runnable(force_refresh=1) def setup(blw:BpyWorld): blw.delete_previous(materials=False) (BpyObj.Plane("Projection") .scale(2, 2) .material("projection_material", lambda m: m .f(0) .specular(0) .animation(varfont_animation))) ================================================ FILE: examples/blender/rome.py ================================================ from coldtype import * from coldtype.blender import BlenderTimeline, b3d_sequencer from coldtype.raster import * from noise import pnoise1 fnt = Font.Find("NCND", "__variables") #fnt = Font.Find("MDPrimer-Bold") fnt = Font.Find("PolymathV") bt = BlenderTimeline(ººBLENDERºº, 5400, fps=24) words = bt.interpretWords(include="+3") action = bt.interpretWords(include="+4") styling = bt.interpretWords(include="+5") #adlib = bt.interpretWords(include="+4") #suggestion = bt.interpretWords(include="+5") rs1 = random_series(0.35, 0.75) @b3d_sequencer((1920, 1080) , timeline=bt , bg=hsl(0.3) , render_bg=False , live_preview_scale=0.35 ) def lyrics(f:Frame): #return None try: word_by_word = action.current(f.i).text.strip() == "words" except: word_by_word = False try: titles = styling.current(f.i).text.strip() == "titles" except: titles = False r = f.a.r.take(170, "S") yellow = hsl(0.14, 1, 0.65) yellow = bw(1) #yellow = bw(0) def ncnd(c): txt = c.text if txt.endswith("plus"): txt = " +" wght = rs1[c.clip.idx] wght = 0.85 if titles: return (txt.upper(), Style(fnt, 32 if "Theo" in txt else 52, wght=0.85, space=2000 if "Rome" in txt else 600)) return (txt.lower(), Style(fnt, 52, wght=wght, space=600)) slug = (words.currentGroup(f.i) .pens(f.i, ncnd) .xalign(r) .cond(titles, lambda p: p.align(f.a.r.take(1, "S")), lambda p: p.align(r, "N")) #.align(r, "N") .cond(word_by_word, lambda p: p.removeFutures()) #.removeFutures() #.fssw(1, 0, 10, 1) .ch(filmjitter(f.e("l", 20), scale=(1.5, 3))) .f(1) .layer( #lambda p: p.fssw(1, 1, 1).ch(phototype(f.a.r, 2, 130, 70, fill=0)).t(o:=1.75, -o).ch(precompose(f.a.r)).ch(fill(bw(0, 0.85))), lambda p: p.ch(phototype(f.a.r, 1.5, 140, 83, fill=yellow)) ) #.pen() #.xor(lambda p: P(p.ambit(tx=1, ty=1).inset(-4, 0))) ) img = SkiaImage(f"~/Video/Theo/rome/v2images/romeimages{(86400 + f.i):08d}.png") analog_slug = ( slug .up() .append(P().rect(f.a.r) .f(-1) .ch(spackle(cut=5, cutw=1, base=f.i, fill=bw(1))) .blendmode(BlendMode.Cycle(35))) .ch(precompose(f.a.r)) .ch(blur(0.5)) .ch(precompose(f.a.r))) return (P( img.copy(), # (P(analog_slug.copy(), # img.ch(phototype(f.a.r, 3, 113, 0)).blendmode(BlendMode.Cycle(9))) # .ch(precompose(f.a.r))), (P(analog_slug.copy().t(0, 0).ch(fill(bw(0))), img.ch(phototype(f.a.r, 10, 70, 11)).blendmode(BlendMode.Cycle(38)) ) .ch(precompose(f.a.r)) .ch(invert()) .ch(precompose(f.a.r)) .ch(blur(1.25))) .ch(precompose(f.a.r)) #.ch(temptone(0.67, 0.36)) #.ch(precompose(f.a.r)) #.ch(expose(1.95+pnoise1(f.e("l", 5))*10)) , )) ================================================ FILE: examples/blender/rome_preview.py ================================================ from coldtype import * from coldtype.blender import BlenderTimeline, b3d_sequencer from coldtype.raster import * from noise import pnoise1 fnt = Font.Find("NCND", "__variables") #fnt = Font.Find("MDPrimer-Bold") fnt = Font.Find("PolymathV") rs1 = random_series(0.35, 0.75) img = SkiaImage(f"~/Downloads/theokeyboard.png") @animation(img.rect() , timeline=Timeline(30) , bg=hsl(0.3) , render_bg=False ) def lyrics(f:Frame): r = f.a.r.take(170, "S") yellow = hsl(0.14, 1, 0.65) yellow = bw(1) #yellow = bw(0) slug = (StSt("Rome Wasn’t Built\nin a Day".upper(), fnt, 302, wght=0.85, space=1400, leading=160) .xalign(r) #.wordPens() #.map(lambda i, p: p.t(0, -i*110)) .align(f.a.r.take(1, "S")) .t(0, 110) .ch(filmjitter(f.e("l", 20), scale=(1.5, 3))) .f(1) .ch(phototype(f.a.r, 1.5, 140, 83, fill=yellow))) analog_slug = ( slug .up() .append(P().rect(f.a.r) .f(-1) .ch(spackle(cut=15, cutw=1, base=f.i, fill=bw(1))) .blendmode(BlendMode.Cycle(35))) .ch(precompose(f.a.r)) .ch(blur(3.5)) .ch(precompose(f.a.r))) return (P( img.copy(), # (P(analog_slug.copy(), # img.ch(phototype(f.a.r, 3, 113, 0)).blendmode(BlendMode.Cycle(9))) # .ch(precompose(f.a.r))), (P(analog_slug.copy().t(0, 0).ch(fill(bw(0))), img.ch(phototype(f.a.r, 60, 100, 11)).blendmode(BlendMode.Cycle(38)) ) .ch(precompose(f.a.r)) .ch(invert()) .ch(precompose(f.a.r)) .ch(blur(1.25))) .ch(precompose(f.a.r)) #.ch(temptone(0.67, 0.36)) #.ch(precompose(f.a.r)) #.ch(expose(1.95+pnoise1(f.e("l", 5))*10)) , )) ================================================ FILE: examples/blender/rotating.py ================================================ from coldtype import * from coldtype.blender import * from coldtype.raster import phototype @b3d_runnable(playback=B3DPlayback.KeepPlaying) def setup(bw:BpyWorld): (bw.delete_previous() .cycles(32, denoiser=False, canvas=Rect(1080/4), transparent=True)) @b3d_animation(Rect(1080), timeline=120, denoise=0, bg=1, upright=1, render_bg=0 , solo=ººBLENDERINGºº) def varfont(f): return (StSt("e", Font.JBMono(), 1200 , wght=f.e("seio", 1)) .align(f.a.r, ty=1) .f(0) .mapv(lambda p: p .ch(b3d(lambda bp: bp .extrude(0.5) .rotate(z=f.e("l", 0, rng=(0, -360))))))) r = Rect(1080) s = Scaffold(r.inset(10)).numeric_grid(9, gap=4, annotate_rings=True) @animation(r, tl=120, bg=hsl(0.7, 0.7, 0.45) , solo=not ººBLENDERINGºº , release=λ.export("h264", loops=8)) def manye_live(f): def img(x): ring = x.el.data("ring") fi = f.adj(-ring*4).e("l", 0, r=(0, 120)) return (varfont.pass_img(ring*-4+round(fi)) .resize(0.77) .align(x.el.r, tx=1, ty=1)) return (P().enumerate(s.cells(), img) .ch(phototype(f.a.r, 0.75, 180, 30, fill=hsl(0.7, 0.7, 0.85)))) ================================================ FILE: examples/blender/sequence.py ================================================ from coldtype import * from coldtype.blender import BlenderTimeline, b3d_sequencer bt = BlenderTimeline(ººBLENDERºº, 120) channel1 = bt.interpretWords(include="+1 +2") @b3d_sequencer((1080, 1080) , timeline=bt , bg=None , render_bg=False , live_preview_scale=0.25 ) def sequence(f:Frame): def setter(c): style = Style(Font.JBMono(), 100, wght=1) if "script" in c.styles: style = Style(Font.RecMono(), 100, wght=1) return [c.text.lower(), style] return (P( channel1 .currentGroup(f.i) .pens(f.i, setter) .align(f.a.r) .removeFutures() .f(1), StSt(str(f.i), Font.JBMono(), 72, wght=1) .align(f.a.r.take(100, "S"), tx=0) .f(1))) ================================================ FILE: examples/blender/sequence_text3d.py ================================================ from coldtype import * from coldtype.blender import * bt = BlenderTimeline(ººBLENDERºº, 120) @b3d_runnable(playback=B3DPlayback.AlwaysStop) def prerun(bw): bw.delete_previous() @b3d_animation((1080, 1080), timeline=bt) def sequence(f:Frame): current = bt.current(1, f.i) return (StSt(current.name, Font.JBMono(), 200 , wght=current.e("eeo", 0)) .align(f.a.r) .pen() .ch(b3d(lambda bp: bp .extrude(1)))) ================================================ FILE: examples/blender/sequence_text3d_rich.py ================================================ from coldtype import * from coldtype.blender import * bt = BlenderTimeline(ººBLENDERºº, 120) channel1 = bt.interpretWords(include="+1 +2") @b3d_runnable(playback=B3DPlayback.AlwaysStop) def prerun(bw): bw.delete_previous(materials=False) @b3d_animation((1080, 1080), timeline=bt) def sequence(f:Frame): try: BpyWorld().delete_previous(materials=False) except: pass default_style = Style(Font.JetBrainsMono(), 100, wght=1) def setter(c): style = default_style if "script" in c.styles: style = Style(Font.RecMono(), 100, wght=1) return [c.text.lower(), style] return (P( channel1 .currentGroup(f.i) .pens(f.i, setter) .align(f.a.r) .removeFutures() .mapv(lambda p: p .declare(font:=str(p.parent().data("style", default_style).font.path)) .ch(b3d(lambda bp: bp .extrude(1.5) .material("mat1" if "JetBrains" in font else "mat2") ))) , StSt(str(f.i), Font.JBMono(), 72, wght=f.e("eeio")) .align(f.a.r.take(200, "S"), tx=0) .f(1) .pen() .ch(b3d(lambda bp: bp .extrude(f.e("l", 0, rng=(0.1, 1))) .material("mat3"))))) ================================================ FILE: examples/blender/simple_single.py ================================================ from coldtype import * from coldtype.blender import * @b3d_animation(tl=60) def scratch(f): return (P() .roundedRect(f.a.r.inset(350), 10) .rotate(f.e("ceio", 1, rng=(-75, 75))) .f(hsl(0.65)) .tag("single_shape") .ch(b3d(lambda p: p .extrude(1.5) .roughness(1) .specular(0) , material="auto"))) ================================================ FILE: examples/blender/simplebeat.py ================================================ from coldtype import * from coldtype.blender import * audio = __sibling__("media/simplebeat.wav") midi = MidiTimeline(__sibling__("media/simplebeat.mid") , bpm=120 , lookup={0: 36, 1: 38, 2: 40, 3: 42, 4: 45, 5: 47, 6: 48, 7: 49}) lengths = [30, 20, 20, 20, 5, 50, 30, 10] weights = [1, 0.25, 0.25, 0.25, 0, 1, 0.5, 0] rs = random_series() @b3d_runnable(playback=B3DPlayback.KeepPlaying) def prerun(bw): bw.deletePrevious(materials=False) @b3d_animation((1080, 1080), timeline=midi, bg=None) def simplebeat2(f): def styler(x): e = f.a.t.ki(x.i) tail = lengths[x.i] return [ Style(Font.MuSan(), 300, wght=0, wdth=0.35, ital=0), Style(Font.MuSan() , fontSize=300 , ro=1 , wght=e.adsr([2, tail], ["sei", "ceo"] , rng=(0, weights[x.i])) , wdth=e.adsr([2, tail], ["sei", "ceo"] , rng=(0, weights[x.i])))] return (Glyphwise("MIDI\nDATA", styler) .lead(30) .xalign(f.a.r, tx=0) .align(f.a.r, tx=0) .collapse() .layer( lambda ps: ps.map(lambda i, p: p # fill .ch(b3d(lambda bp: bp .extrude(2) .locate(z=-5+5*f.a.t.ki(i) .adsr([5, lengths[i]*2], ["sei", "seo"]))))))) ================================================ FILE: examples/blender/timedtext.py ================================================ from coldtype import * from coldtype.blender import * from coldtype.timing.sequence import ClipGroupTextSetter # Blender # - Switch to "Video Editing" # - View "Tool" in Sequencer # - CT2D: Hit "Settings > Defaults" # - Author data # - CT2D: Import > Preview # - Create Image Editor, select image live preview # - CT2D: Render > Film reel (render all) # - CT2D: Import > Frames # Gotchas # Only set timeline length and fps in Coldtype code # (not in Blender itself) bt = BlenderTimeline(ººBLENDERºº, 130, 30) words = bt.interpretWords(include="+1 +2 +3") @b3d_sequencer((1080, 1080) , timeline=bt , bg=0 , live_preview_scale=0.1 ) def timedtext(f:Frame): def styler(c:ClipGroupTextSetter): font, fs = "PolymathV", 100 blue = c.styles.ki("blue") ital = c.styles.ki("italic").e("seo", 0) wght = 1 if c.styles.ki("static") else Easeable(c.clip, f.i).e("sei", 0, rng=(0, 1)) return (c.text, Style(font, fs , wght=wght , ital=ital , fill=hsl(0.6) if blue else hsl(c.clip.idx*0.025))) return (P( words.currentGroup(f.i) .pens(f.i, styler) .lead(10) .xalign(f.a.r, "W", tx=0) .align(f.a.r, tx=0) .remove_futures())) ================================================ FILE: examples/blender/varfont.py ================================================ from coldtype import * from coldtype.blender import * @b3d_runnable(playback=B3DPlayback.KeepPlaying) def prerun(bw): bw.delete_previous(materials=False) @b3d_animation(timeline=60, denoise=0) def varfont(f): return (StSt("ABC\nDEF", Font.ColdtypeObviously() , fontSize=f.e("seio", 1, rng=(300, 500)) , wdth=f.e("seio", 1, rng=(1, 0))) .align(f.a.r) .f(1) .mapv(lambda p: p .ch(b3d(lambda bp: bp .extrude(f.e("seio", 1, rng=(0.5, 5.75))))))) ================================================ FILE: examples/blender/varfont2.py ================================================ from coldtype import * from coldtype.blender import * @b3d_runnable(playback=B3DPlayback.KeepPlaying) def prerun(bw:BpyWorld): bw.delete_previous(materials=False) @b3d_animation(timeline=60, center=(0, 1), upright=1) def varfont2(f): return (P( Glyphwise("COLD\nTYPE", lambda g: Style(Font.ColdtypeObviously(), 375 , wdth=f.adj(-g.i*5).e("seio", 1, rng=(0.98, 0)))) .xalign(f.a.r) .track(50, v=1) .align(f.a.r.drop(100, "S")) .mapv(lambda i, p: p .ch(b3d(lambda bp: bp .extrude(f.adj(-i*5) .e("seio", 1, rng=(0.1, 1.75)))))), StSt("Now in 3D!", Font.RecursiveMono(), 100) .align(f.a.r.take(0.2, "S")) .pen() .f(hsl(0.65, f.e("eeio", 1), 0.6)) .ch(b3d(lambda bp: bp .extrude(f.e("eeio", 1, rng=(0.1, 3))) .specular(0) .roughness(1) .metallic(f.e("eeio", 1)) , material="auto")) .hide() )) ================================================ FILE: examples/blender/wip/bake.py ================================================ from coldtype import * from coldtype.blender import * @b3d_runnable() def setup(bw:BpyWorld): bw.deletePrevious("CTBakedAnimation_baketest2") @b3d_animation(timeline=60 , reset_to_zero=1 , bake=True) # switch to True def baketest2(f): return (StSt("AV", Font.MutatorSans(), 200, wdth=f.e("eeio", 1), wght=f.e("eeio", 2), ro=1) .align(f.a.r, ty=1) .mapv(lambda p: p .ch(b3d(lambda bp: bp .rotate(90) .extrude(f.e("seio", 1, rng=(0.1, 1.5))) , material="letters")))) ================================================ FILE: examples/blender/wip/blends/boston.blend.json ================================================ { "start": 0, "end": 274, "livepreview_disabled": false, "tracks": [ { "index": 1, "clips": [ { "name": "*p\u02b0liz", "text": "*p\u02b0liz", "start": 10.0, "end": 19 }, { "name": "\u2248kal\u02e0", "text": "\u2248kal\u02e0", "start": 19.0, "end": 27 }, { "name": "\u2248st\u025b", "text": "\u2248st\u025b", "start": 27.0, "end": 31 }, { "name": "+l\u0258", "text": "+l\u0258", "start": 31.0, "end": 44 }, { "name": "*\u00e6sk", "text": "*\u00e6sk", "start": 44.0, "end": 49 }, { "name": "+.001", "text": "+ h\u025a", "start": 49.0, "end": 52 }, { "name": "\u027e\u0258", "text": "\u027e\u0258", "start": 52.0, "end": 56 }, { "name": "\u2248b\u0279\u026a\u014b", "text": "\u2248b\u0279\u026a\u014b", "start": 56.0, "end": 62 }, { "name": "\u00f0i\u02d0z", "text": "\u00f0i\u02d0z", "start": 62.0, "end": 69 }, { "name": "\u03b8\u026a\u014b\u0261z", "text": "\u03b8\u026a\u014b\u0261z", "start": 69.0, "end": 76 }, { "name": "\u2248w\u026a\u00f0", "text": "\u2248w\u026a\u00f0", "start": 76.0, "end": 80 }, { "name": "h\u025a.001", "text": "h\u025a", "start": 80.0, "end": 86 }, { "name": "f\u0279\u0254m", "text": "f\u0279\u0254m", "start": 86.0, "end": 91 }, { "name": "\u2248n\u0258", "text": "\u2248n\u0258", "start": 91.0, "end": 97 }, { "name": "st\u0254\u0258", "text": "st\u0254\u0258", "start": 97.0, "end": 112 }, { "name": "*s\u026aks", "text": "*s\u026aks", "start": 112.0, "end": 122 }, { "name": "spu\u02d0nz", "text": "spu\u02d0nz", "start": 122.0, "end": 130 }, { "name": "\u2248\u028cv", "text": "\u2248\u028cv", "start": 130.0, "end": 136 }, { "name": "f\u0279\u025b\u0283", "text": "f\u0279\u025b\u0283", "start": 136.0, "end": 144 }, { "name": "\u2248sno\u028a", "text": "\u2248sno\u028a", "start": 144.0, "end": 151 }, { "name": "p\u02b0i\u02d0z", "text": "p\u02b0i\u02d0z", "start": 151.0, "end": 169 }, { "name": "*fa\u026av", "text": "*fa\u026av", "start": 169.0, "end": 175 }, { "name": "t\u026ak", "text": "t\u026ak", "start": 175.0, "end": 184 }, { "name": "\u2248sl\u00e6bz", "text": "\u2248sl\u00e6bz", "start": 184.0, "end": 190 }, { "name": "\u028cv.001", "text": "\u028cv", "start": 190.0, "end": 194 }, { "name": "\u2248blu", "text": "\u2248blu", "start": 194.0, "end": 202 }, { "name": "t\u0283i\u02d0z", "text": "t\u0283i\u02d0z", "start": 202.0, "end": 208 }, { "name": "*n", "text": "*n", "start": 208.0, "end": 212 }, { "name": "me\u026a", "text": "me\u026a", "start": 212.0, "end": 215 }, { "name": "+bi", "text": "+bi", "start": 215.0, "end": 218 }, { "name": "\u0258", "text": "\u0258", "start": 218.0, "end": 222 }, { "name": "\u2248sn\u00e6k", "text": "\u2248sn\u00e6k", "start": 222.0, "end": 229 }, { "name": "f\u0254\u0279", "text": "f\u0254\u0279", "start": 229.0, "end": 232 }, { "name": "+", "text": "+ h\u025a", "start": 232.0, "end": 235 }, { "name": "\u2248b\u0279\u028c", "text": "\u2248b\u0279\u028c", "start": 235.0, "end": 238 }, { "name": "+\u00f0\u0258", "text": "+\u00f0\u0258", "start": 238.0, "end": 241 }, { "name": "b\u0254\u0258b", "text": "b\u0254\u0258b", "start": 241.0, "end": 262 } ] }, { "index": 2, "clips": [ { "name": "*please", "text": "*please", "start": 10.0, "end": 19 }, { "name": "call", "text": "call", "start": 19.0, "end": 27 }, { "name": "ste", "text": "ste", "start": 27.0, "end": 31 }, { "name": "+lla", "text": "+lla", "start": 31.0, "end": 44 }, { "name": "*ask", "text": "*ask", "start": 44.0, "end": 49 }, { "name": "her", "text": "her", "start": 49.0, "end": 52 }, { "name": "to", "text": "to", "start": 52.0, "end": 56 }, { "name": "bring", "text": "bring", "start": 56.0, "end": 62 }, { "name": "these", "text": "these", "start": 62.0, "end": 69 }, { "name": "things", "text": "things", "start": 69.0, "end": 76 }, { "name": "\u2248with", "text": "\u2248with", "start": 76.0, "end": 80 }, { "name": "her.001", "text": "her", "start": 80.0, "end": 86 }, { "name": "from", "text": "from", "start": 86.0, "end": 91 }, { "name": "the", "text": "the", "start": 91.0, "end": 97 }, { "name": "store", "text": "store", "start": 97.0, "end": 112 }, { "name": "*six", "text": "*six", "start": 112.0, "end": 122 }, { "name": "spoons", "text": "spoons", "start": 122.0, "end": 130 }, { "name": "of", "text": "of", "start": 130.0, "end": 136 }, { "name": "fresh", "text": "fresh", "start": 136.0, "end": 144 }, { "name": "snow", "text": "snow", "start": 144.0, "end": 151 }, { "name": "peas", "text": "peas", "start": 151.0, "end": 169 }, { "name": "*five", "text": "*five", "start": 169.0, "end": 175 }, { "name": "thick", "text": "thick", "start": 175.0, "end": 184 }, { "name": "slabs", "text": "slabs", "start": 184.0, "end": 190 }, { "name": "\u2248of.001", "text": "\u2248of", "start": 190.0, "end": 194 }, { "name": "blue", "text": "blue", "start": 194.0, "end": 202 }, { "name": "cheese", "text": "cheese", "start": 202.0, "end": 208 }, { "name": "*and", "text": "*and", "start": 208.0, "end": 212 }, { "name": "may", "text": "may", "start": 212.0, "end": 215 }, { "name": "+be", "text": "+be", "start": 215.0, "end": 218 }, { "name": "a", "text": "a", "start": 218.0, "end": 222 }, { "name": "snack", "text": "snack", "start": 222.0, "end": 229 }, { "name": "\u2248for", "text": "\u2248for", "start": 229.0, "end": 232 }, { "name": "her.002", "text": "her", "start": 232.0, "end": 235 }, { "name": "bro", "text": "bro", "start": 235.0, "end": 238 }, { "name": "+ther", "text": "+ther", "start": 238.0, "end": 241 }, { "name": "bob", "text": "bob", "start": 241.0, "end": 262 } ] }, { "index": 5, "clips": [ { "name": ".accent", "text": ".accent", "start": 49.0, "end": 52 }, { "name": ".001", "text": ".accent", "start": 97.0, "end": 112 }, { "name": ".002", "text": ".accent", "start": 241.0, "end": 262 } ] }, { "index": 6, "clips": [ { "name": ".emphasis", "text": ".emphasis", "start": 241.0, "end": 262 } ] } ] } ================================================ FILE: examples/blender/wip/boston.py ================================================ from coldtype import * from coldtype.blender import * from coldtype.timing.sequence import ClipGroupTextSetter # sound: https://accent.gmu.edu/browse_language.php?function=detail&speakerid=79 # typed with: https://westonruter.github.io/ipa-chart/keyboard/ """ pʰliz kalˠ stɛlɘ æsk hɚ ɾɘ bɹɪŋ ðiːz θɪŋɡz wɪð hɚ fɹɔm nɘ stɔɘ sɪks spuːnz ʌv fɹɛʃ snoʊ pʰiːz faɪv tɪk slæbz ʌv blu tʃiːz n meɪbi ɘ snæk fɔɹ hɚ bɹʌðɘ bɔɘb """ bt = BlenderTimeline(ººBLENDERºº, 275) ipa = bt.interpretWords(include="+1 +5 +6") english = bt.interpretWords(include="+2") @b3d_sequencer((1080, 1080) , timeline=bt , bg=hsl(0.6, 0.7, 0.2) , live_preview_scale=0.25 , audio=ººsiblingºº("../media/pleasecallstella.wav") ) def lyrics(f:Frame): def ipa_styler(c:ClipGroupTextSetter): font, fs = "Brill Roman", 200 if c.styles.ki("accent"): font, fs = "Brill Italic", 400 if c.styles.ki("emphasis"): font = "Brill Bold Italic" return (c.text, Style(font, fs)) return (P( english.currentGroup(f.i) .pens(f.i, lambda c: (c.text, Style("PolymathV", 60, wght=0.35, opsz=0))) .lead(10) .xalign(f.a.r) .align(f.a.r.take(0.35, "S")) .remove_futures() .f(1), ipa.currentWord(f.i) .pens(f.i, ipa_styler, styles=ipa.styles) .lead(40) .xalign(f.a.r) .align(f.a.r.take(0.85, "N")) .remove_futures() .f(1))) ================================================ FILE: examples/blender/wip/bump.py ================================================ from coldtype import * from coldtype.blender import * r = Rect(1080, 540) font = Font.ColdObvi() @renderable(r, bg=0, render_bg=1) def graphic(r): return (StSt("TYPE", font, 250) .f(1) .align(r)) @b3d_runnable(force_refresh=True) def setup(blw:BpyWorld): (blw.delete_previous(materials=False) .timeline(Timeline(50, 12) , output=__FILE__ , version=1)) (BpyObj.Plane("ImagePlane") .scale(2, 1) .material("asdf_image", lambda m: m .image(graphic, emission=0, render=True))) ================================================ FILE: examples/blender/wip/physics.py ================================================ from coldtype import * from coldtype.blender import * txt = "FALL\nING\nTEXT" @b3d_runnable() def setup(bpw:BpyWorld): (bpw.delete_previous() .timeline(Timeline(120)) .rigidbody(speed=3, frame_end=1000)) (BpyObj.Find("Plane") .rigidbody("passive", friction=1, bounce=0)) @b3d_renderable(reset_to_zero=1) def falling(r): return (StSt(txt, Font.MutatorSans(), 300, wght=1) .track(110, v=1) .map(lambda p: p.trackToRect(r.inset(70))) .align(r.inset(50)) .deblank() .mapv(lambda p: p .ch(b3d(lambda bp: bp .extrude(0.275) .convert_to_mesh() .rigidbody(friction=0.5) , zero=True)) .ch(b3d_post(lambda bp: bp .locate_relative(z=10))))) ================================================ FILE: examples/blender/wip/physics_semi2d.py ================================================ from coldtype import * from coldtype.blender import * txt = "LETTERS\nCOLORFUL\nA LOT OF\nCOME\nHERE" @b3d_runnable() def setup(bw:BpyWorld): (bw.delete_previous() .timeline(Timeline(150), resetFrame=0) .rigidbody(speed=0.5, frame_end=1000)) #@b3d_renderable(center=(0, 1), upright=1) def justi(r): #return None letters = (StSt(txt, "Streco", 150) .xalign(r) .track(1000, v=1) .align(r.inset(50), y="S") .t(0, 1080) .deblank() .collapse() .map(lambda p: p.explode()) .collapse() .mapv(lambda i, p: p .tag(f"glyph_{i}") .f(hsl(random())) .ch(b3d(lambda bp: bp .extrude(0.35) .convertToMesh() .rigidbody(friction=0.2, bounce=0.2) .specular(0) .roughness(1) , dn=True , upright=1 , zero=True , material="auto")))) def wall(tag, _r, depth=0.5, yShift=0): return (P(_r).tag(tag) .ch(b3d(lambda bp: bp .extrude(depth) .convertToMesh() .rigidbody("passive") , material="wall_material")) .ch(b3d_post(lambda bp: bp .locate_relative(y=yShift) .hide() ))) return (P( letters, # (P( # wall("_front_wall", r, 0.01, 0.4), # wall("_back_wall", r, 0.01, -0.4), # wall("_floor", r.take(50, "S")), # wall("_west_wall", r.take(50, "W")), # wall("_east_wall", r.take(50, "E")))) )) ================================================ FILE: examples/blender/wip/physics_visible.py ================================================ from coldtype.blender.fluent import BpyObj from coldtype import * from coldtype.blender import * from functools import partial """ A two-pass physics animation: - when LIVE_PHYSICS is True, a single lockup is rendered to blender for a normal physics simulation — once you’ve got this looking how you want it, you can select all the glyph objects and then in the 3d view, select the Object dropdown, and then in the Rigid Body menu, select "Bake to Keyframes"; once that’s complete, you can hide all the glyphs, then come back to this file and set LIVE_PHYSICS = False, then resave - when LIVE_PHYSICS is False (and the rigid body simulation has been baked to keyframes), a variable font animation uses the baked keyframes to "fake" the rigid body physics simulation (since (as far as I know) it’s impossible to do a "true" variable font / callback-based rigid body simulation in blender) """ LIVE_PHYSICS = True t = Timeline(65) r = Rect(1080, 1080) def build_lockup(wght): return (StSt("FALLING", Font.MutatorSans(), 120 , wdth=1 , wght=wght) .align(r.inset(50)) .translate(0, 500)) if LIVE_PHYSICS: @b3d_runnable() def before_bake(bw): (bw.deletePrevious() .timeline(t, resetFrame=0) .renderSettings(128) .rigidbody(speed=2, frame_end=1000)) @b3d_renderable(center=(0, 1), upright=1) def physics_basis(r): letters = (build_lockup(0) .pmap(lambda i, p: p .tag(f"glyph_{i}") .ch(b3d(lambda bp: bp .extrude(0.5) .convertToMesh() .locate_relative(z=i*0.25) .rigidbody(friction=1, bounce=0.95), dn=True, upright=True, zero=True)))) return P( letters, (P(r.take(200, "S")) .tag("surface") .ch(b3d(lambda bp: bp .extrude(0.1) .convertToMesh() .rigidbody("passive") , material="surface_material")) .ch(b3d_post(lambda bp: bp .rotate(x=88) .locate_relative(z=-2))))) else: @b3d_animation(timeline=t, center=(0, 1), upright=1) def physics_upright_curves(f): def positioner(i, bp): ref = BpyObj.Find(f"glyph_{i}").obj bp.locate(ref.location) bp.rotate(ref.rotation_euler) return bp return (build_lockup(f.e("eeio", 2)) .pmap(lambda i, p: p .tag(f"curve_{i}") .ch(b3d(lambda bp: bp.extrude(0.5), upright=True, zero=True)) .ch(b3d_post(partial(positioner, i))))) ================================================ FILE: examples/blender/wip/timed3d.py ================================================ from coldtype import * from coldtype.blender import * """ Some very simple timed-text-in-3d """ bt = BlenderTimeline(__BLENDER__, 80) @b3d_animation((1080, 1080), timeline=bt) def timed(f:Frame): def styler(c): return c.text.upper(), Style(Font.MutatorSans(), 250) return (f.t.words.currentGroup() .pens(f, styler) .align(f.a.r) .removeFutures() .pen() .copy() .ch(b3d(lambda p: p.extrude(1)))) ================================================ FILE: examples/blog.py ================================================ from coldtype import * from fontTools.ufoLib import UFOReader logos = UFOReader("assets/logos.ufo").getGlyphSet() @renderable((1200, 600), bg=0) def nameplate(r, fontSize=500, wdth=0.25, rotate=0): return (P( StSt("COLDTYPE", Font.ColdObvi(), fontSize , wdth=wdth , rotate=rotate , tu=-50, r=1) .fssw(1, 0, 20, 1) .align(r), P().glyph(logos["goodhertz_logo_2019"]) .scale(0.5) .align(r) .f(hsl(0.61, 0.7, 0.6)) .blendmode(BlendMode.Multiply))) ================================================ FILE: examples/borders.py ================================================ from coldtype import * @renderable((500, 500)) def borders(r): s = Scaffold(r) s.cssgrid("a a", "a a", "a | b __/ c | d", a=("a a", "a a", "a_ || b / c || d")) s["d"].cssgrid("a a", "a a", "a | b _/ c d") s["a/d"].cssgrid("a a", "a a", "a b _/ c | d") reg = lambda p: p.fssw(-1, hsl(0.5), 10) bold = lambda p: p.fssw(-1, hsl(0.9), 20) s["b"].grid(4, 4) s["c"].numeric_grid(5) return P( s.view(fontSize=18, fill=False), s["a"].cssborders(lambda p: p.fssw(-1, hsl(0.7), 5), -1), s["a/d"].cssborders(), s["d"].cssborders(), s["a"].cssborders(-1, bold), s.cssborders(reg, bold), s["b"].borders().fssw(-1, hsl(0.07, 0.7), 2), s["c"].borders().fssw(-1, hsl(0.7), 1)) ================================================ FILE: examples/chessboard.py ================================================ from coldtype import * from string import ascii_uppercase fill = hsl(0.38, 0.7, 0.7) chessfont = Font.LibraryFind(r"AppleSymbols") @renderable(bg=1) def board(r): def cell(x): if x.el.data("checker"): return P().oval(x.el.r).f(fill) else: return P().rect(x.el.r.inset(1)).f(fill) s = Scaffold(r.inset(100)).numeric_grid(8, gap=20) board = P().enumerate(s.cells(), cell) borders = s.borders().ssw(hsl(0.6, 0.6, 0.7), 2) style = Style(Font.JBMono(), 30, wght=1) labels = (P( P().enumerate(s.rows()[0], lambda x: StSt(ascii_uppercase[x.i], style) .align(x.el.r) .f(0) .t(0, x.el.r.h-10) .rotate(180)), P().enumerate(s.rows()[-1], lambda x: StSt(ascii_uppercase[x.i], style) .align(x.el.r) .f(0) .t(0, -x.el.r.h+10)), P().enumerate(s.cols()[0], lambda x: StSt(str(8-x.i), style) .align(x.el.r) .f(0) .t(-x.el.r.w+10, 0)), P().enumerate(s.cols()[-1], lambda x: StSt(str(8-x.i), style) .align(x.el.r) .f(0) .t(x.el.r.w-10, 0) .rotate(180)))) rows = s.rows() player1 = (P().enumerate("♖♘♗♕♔♗♘♖", lambda x: StSt(x.el, chessfont, 50) .align(rows[0][x.i]) .rotate(180) .f(0))) pawns1 = (StSt("♙", chessfont, 50).f(0) .pen() .unframe() .rotate(180) .replicate(rows[1])) player2 = (P().enumerate("♜♞♝♛♚♝♞♜", lambda x: StSt(x.el, chessfont, 50) .align(rows[-1][x.i]) .f(0))) pawns2 = (StSt("♟", chessfont, 50).f(0) .pen() .unframe() .replicate(rows[-2])) return P(borders, board, labels, player1, pawns1, player2, pawns2) ================================================ FILE: examples/circle_text.py ================================================ from coldtype import * @renderable((540, 540), bg=1) def circles(r): circle = (P().oval(r.inset(140)) .fssw(-1, hsl(0.9, a=0.5), 6)) return (P( circle, StSt("ABC", Font.MuSan(), 100, wght=0.5, wdth=0, tu=340) .distribute_on_path(circle.copy().scale(1.1), baseline=1) .f(hsl(0.7)), StSt("ABC", Font.MuSan(), 100, wght=0.5, wdth=0, tu=500) .distribute_on_path(circle.copy().scale(0.9).rotate(-180), 0, baseline=0) .f(hsl(0.3)))) ================================================ FILE: examples/colrv1_arabic.py ================================================ from coldtype import * text1 = """ از آنجا که عدم شناسائی و تحقیر حقوق بشر منتهی به اعمال وحشیانه‌ای """ @renderable((1080, 340), bg=1) def arefRuqaa(r): return (StSt(text1, "ArefRuqaaInk-Bold", 80, strip=True) .xalign(r) .lead(60) .align(r)) @renderable((1080, 340), bg=1) def reemKufi(r): return (StSt(text1, "ReemKufiInk-Regular", 70, strip=True) .xalign(r) .lead(60) .align(r)) ================================================ FILE: examples/cropandrepeat.py ================================================ from coldtype import * from coldtype.fx.skia import phototype @aframe(bg=hsl(0.93, 0.6, 0.5)) def scratch(f): def crop(e, p): _e = 10+ez(e, "eei") * p.ambit(tx=0, ty=1).h _crop = P(p.ambit(tx=0, ty=1) .take(_e, "N", forcePixel=1)) return (p .intersection(_crop) .unframe() .align(f.a.r, "S", tx=1) .t(ez(e, "seio", 1)*470, 0)) return (StSt("CROP AND REPEAT", Font.MuSan() , fontSize=170 , wght=0 , wdth=1 , fit=f.a.r.w) .layer(47) .mape(crop) .stack(4) .scale(0.7) .align(f.a.r.drop(10, "S"), "S") .rotate(0.25) .f(1) .ch(phototype(f.a.r, 1.5, 105, 30 , fill=hsl(0.17, 0.8, 0.65)))) ================================================ FILE: examples/custom_hotkey.py ================================================ from coldtype import * @renderable((100, 100), bg=hsl(0.9)) def scratch(r): return None def custom_hotkey(key, renderer): print(">>>", key, renderer) ================================================ FILE: examples/diagram.py ================================================ from coldtype import * import coldtype.fx.diagram as d r1 = random_series(seed=5) @renderable((1080, 540), bg=1) def d1(r): return (P().oval(Rect(120)) .layer(3).spread(70) .layer(2).stack(60) .mapv(lambda i, p: p.f(hsl(r1[i], 0.7, 0.7))) .index(0, lambda p: p.ch(d.interconnect())) .index(1, lambda p: p.reverse() .ch(d.interconnect("←→←"))) .append(lambda p: d.ujoin(p[0], p[1], "→", 50, "-←")) .append(lambda p: d.ujoin(p[1], p[0], "←", 50, "-→")) .align(r)) ================================================ FILE: examples/direct_uharfbuzz.py ================================================ from coldtype import * import uharfbuzz as hb ct_font = Font.Find("JetBrainsMono.ttf", max_depth=5) blob = hb.Blob.from_file_path(ct_font.path) face = hb.Face(blob) @renderable(bg=1) def direct(r:Rect): font = hb.Font(face) ttFont = ct_font.font.ttFont cap_height = ttFont['OS/2'].sCapHeight max_wght = ct_font.variations()["wght"]["maxValue"] font.set_variations(dict(wght=max_wght)) buf = hb.Buffer() buf.add_str("bag") buf.guess_segment_properties() features = {"kern": True, "liga": True} hb.shape(font, buf, features) infos = buf.glyph_infos positions = buf.glyph_positions def glyph(x): info, pos = x.el gid = info.codepoint p = P() font.draw_glyph_with_pen(gid, p) p.translate(pos.x_offset, pos.y_offset) # set the typographical "frame" (this is a special property that .align calls check for) p.data(frame=Rect(pos.x_offset, pos.y_offset, pos.x_advance, cap_height)) return p return (P().enumerate(zip(infos, positions), glyph) .spread(60) # 60 is equivalent to display-space tracking .scale(0.5, pt=(0,0)) .align(r, tx=0) .f(0) .mapv(lambda p: p .up() .insert(0, P().rect(p.ambit()).fssw(-1, hsl(0.9, 0.7, 0.9), 2)))) ================================================ FILE: examples/drawbot/both.py ================================================ from coldtype import * from coldtype.drawbot import * """ The same variable font animation with drawBot and Coldtype """ @drawbot_animation((1080, 540) , timeline=Timeline(30) , bg=hsl(0.8, 0.6, 0.85) , render_bg=1 ) def bounce(f): # Using DrawBot API co = Font.ColdObvi() fontSize = 190 fs = db.FormattedString("COLDTYPE", font=co.path, fontSize=fontSize, fontVariations=dict( wdth=f.e("eeio", rng=(0, 1000)))) bp = db.BezierPath() bp.text(fs, (50, 50)) db.fill(*hsl(0.4)) db.drawPath(bp) # Using Coldtype API (StSt("COLDTYPE", co, fontSize , wdth=f.e("eeio")) .t(50, 210) .f(hsl(0.4)) .ch(dbdraw)) ================================================ FILE: examples/drawbot/composition.py ================================================ from coldtype import * from coldtype.drawbot import * co = Font("assets/ColdtypeObviously.designspace") long_txt = """This code is a mix of DrawBot and Coldtype, meant to demonstrate that Coldtype primitives (like designspace-reading) can be combined with DrawBot primitives like multi-line text support and the Mac Font library.""" @drawbot_renderable(rect=(900, 500), bg=0) def composition(r): ri = r.inset(20, 20) db.fill(*hsl(0.5)) db.oval(*ri.take(0.5, "mxx").square()) im = db.ImageObject() with im: db.size(500, 500) db.image("https://static.goodhertz.co/statics/store/img/logos/hertz-color-danico-250-lssy.png", (0, 0)) im.photoEffectMono() x, y = im.offset() db.image(im, (250+x, -10+y)) db.fontSize(19) db.font("Georgia-Italic") db.fill(*hsl(0.9, s=0.7, l=0.6)) db.textBox(long_txt, ri.take(200, "mny").take(250, "mnx")) (StSt("COLDTYPE", co, 150, ro=1) .align(ri, "mnx", "mxy") .f(Gradient.Horizontal(ri, hsl(0.05, s=0.7, l=0.6), hsl(0.15, s=0.7, l=0.6))) .s(0) .sw(3) .ch(dbdraw)) (StSt("COLDTYPE", co, 50, ro=1, r=1, tu=-100) .align(ri, "mnx", "mdy") .translate(0, 40) .f(0) .understroke(s=1, sw=5) .ch(dbdraw)) """ When you’re ready to create a PDF, just hit the "R" key in the viewer app """ def release(_): db.saveImage("~/Desktop/composition.pdf") ================================================ FILE: examples/drawbot/pdfdoc.py ================================================ from coldtype import * from coldtype.drawbot import * font = Font.RecursiveMono() @drawbot_animation("letter") def multipage_doc(f): c = hsl(f.e("l", 0)) P(f.a.r.inset(10)).f(c).outline(10).ch(dbdraw) fontName = db.installFont(str(font.path)) fs = db.FormattedString( f"This is page {f.i}" , font=fontName , fontSize=50 , fill=c.rgba()) db.textBox(fs, f.a.r.inset(50)) bp = db.BezierPath() bp.textBox(fs, f.a.r.inset(50, 100)) db.fill(*c) db.drawPath(bp) (StSt(f"This is page {f.i}", font, 50) .f(c) .align(f.a.r.inset(50, 160), "NW", tx=0) .ch(dbdraw)) # hit the 'R' key in the viewer to trigger this def release(_): pdfdoc(multipage_doc, __sibling__("multipage.pdf")) ================================================ FILE: examples/drawbot/pixellation.py ================================================ from coldtype import * from coldtype.drawbot import * @drawbot_animation((1080, 540), bg=0.8, timeline=50) def db_script_test(f): e = f.e() (StSt("COLDTYPE", Font.ColdObvi(), 150, tu=100+-730*e, ro=1, r=1) .align(f.a.r) .rotate(15*(1-e)) .skew(e*0.75) .f(hsl(e, s=1, l=1-e*0.35)) .understroke(sw=20) .scale(1-1*e*0.5) .chain(dbdraw_with_filters(f.a.r, [["pixellate", dict(scale=5.0+5.0*e)]]))) ================================================ FILE: examples/drawbot/varfont.py ================================================ # Installing as little of Coldtype as possible, to leave breathing room for the 'from drawBot import *' from coldtype import Font from coldtype.drawbot import drawbot_animation from drawBot import * mutator = Font.MutatorSans() @drawbot_animation(timeline=60, bg=1) def db_varfont(f): fontName = installFont(str(mutator.path)) fs = FormattedString() fs.fontSize(f.e("ceio", 1, rng=(150, 250))) fs.font(fontName) fs.fontVariations( wdth=f.e("seio", 1, rng=(0, 1000))) fs.append("DRAWBOT AND COLDTYPE") bp = BezierPath() bp.textBox(fs, f.a.r.inset(50, 50).expand(100, "mny")) drawPath(bp) ================================================ FILE: examples/easing.py ================================================ from coldtype import * @renderable((1080, 540), bg=1) def easing(r): sq = Rect(50, 50) return (P().oval(sq) .outline(4) .layer(17) .spread(4) .mape(lambda e, p: p .f(hsl(0.65+ez(e, "ceio"), 0.9, 0.5)) .t(0, ez(e, "eeio", r=(0, 300)))) .align(r)) ================================================ FILE: examples/example.py ================================================ from coldtype import * @renderable(rect=(900, 500)) def coldtype(r): return (StSt("COLDTYPE", Font.ColdObvi() , fontSize=450 , wdth=1 , tu=-50 , r=1 , ro=1 , fit=r.w) .align(r) .mape(lambda e, p: (p .f(hsl(0.5+e*0.15, s=0.6, l=0.55)) .s(0).sw(30).sf(1))) .rotate(5) .scale(0.75)) ================================================ FILE: examples/freeze.py ================================================ from coldtype import * from coldtype.fx.skia import freeze # Freeze! # Kind of like freezing a track in a DAW, # this lets you wrap expensive drawings with a freeze function # that rasterizes to an image and saves that image in-memory # (cache keyed by the source of the lambda passed in) # thereby substituting a calculated vector with its "frozen" # rasterized representation r = Rect(1080, 540) rs1 = random_series(r.mnx-100, r.mxx, 10+int(random()*40)) rs2 = random_series(r.mny-100, r.mxy, 1) rs3 = random_series(seed=3) rs4 = random_series(seed=2) rs5 = random_series(0.50, 0.70, seed=4) @renderable(r) def scratch(r): def color(h): return hsl(h, 0.95, a=0.15) def freezeable(): return (P().enumerate(range(0, 5000), lambda x: StSt("F", Font.MuSan(), 120, wght=rs3[x.i], wdth=rs4[x.i]) .t(rs1[x.i], rs2[x.i]) .f(color(rs5[x.i])))) return (P( freeze(1, 1, freezeable, additionals=[color, rs5]), StSt("F IS FROZEN", Font.MuSan(), 10+random()*100) .align(r).f(1))) ================================================ FILE: examples/github_social.py ================================================ from coldtype import * from coldtype.fx.skia import phototype @renderable((1280, 640), bg=hsl(0.7, 0.6, 0.4), render_bg=True) def github_social(r): def styler(x): e = 1 - x.e if not x.l else x.e return Style(Font.ColdObvi(), 290, wdth=e, tu=-100*e, rotate=30 if x.l else -10) logos = (Glyphwise("COLDTYPE\nCOLDTYPE", styler) .lead(10) .align(r.take(0.9, "N")) .index(1, lambda p: p.t(15, 0)) .fssw(1, 0, 10, 1) .reverse(recursive=True) .ch(phototype(r, blur=3, cut=180, cutw=17))) return P(logos, StSt("A PYTHON LIBRARY", Font.MuSan(), 50, wdth=1) .f(1) .align(r.take(120, "S"))) ================================================ FILE: examples/grid_shapes.py ================================================ from coldtype import * @renderable(bg=1) def shapes(r:Rect): s = Scaffold(r).numeric_grid(10, 10) return (P( s.view(), P(s["5|4*3|2"]).f(bw(0, 0.5)), P(s["4|3*3|2"]).f(bw(0, 0.25)), P(s["0|-1*3|-3"]).f(hsl(0.75, a=0.5)), P(s["-1|-2*-3|-3"]).f(hsl(0.75, a=0.5)), P(s["3|1*2|2"]).f(hsl(0.3, a=0.5)), P(s["-1|0*-2|2"]).f(hsl(0.3, a=0.5)), )) ================================================ FILE: examples/image_in_path.py ================================================ from coldtype import * from coldtype.raster import * @renderable(bg=0) def image(r): shape = P().oval(r.inset(100)).align(r, ty=1) pts = shape.flatten(3).point_list(random_seed=0) return (P() .m(pts[0]) .enumerate(pts[1:], lambda x: x.parent .l(x.el)) .ep() .fssw(-1, 1, 1) .ch(phototype(r, 1, 170, 50))) @renderable(bg=0, layer=0) def in_path(r): return (StSt("COLD\nTYPE", Font.ColdObvi(), 340, wdth=1, fit=r.inset(200).w) .align(r, ty=1) .img(image.render_to_disk()[0], r, pattern=1) .f(0.1) .ch(luma(r, hsl(0.57, 1, 0.50)))) ================================================ FILE: examples/image_rotate.py ================================================ from coldtype import * from coldtype.img.skiaimage import SkiaImage from coldtype.fx.skia import precompose, temptone @renderable((540, 540/2), bg=hsl(0.3), render_only=0) def test_image(r): return StSt("HELLO", Font.MuSan(), 100, wght=1).align(r, ty=1).f(hsl(0.9, 0.6, 0.6)) @animation((1080, 1080), tl=36) def rotate(f): src = test_image.render_to_disk(render_bg=1)[0] return (SkiaImage(src) .resize(1) .align(f.a.r) .rotate(45-f.i*10) #.in_pen().fssw(bw(0, 0.1), 0, 1) .ch(precompose(f.a.r)) .ch(temptone(-0.20, 0.70))) @animation((540, 540)) def resize(f): return (SkiaImage(test_image.pass_path(0)) .resize(0.85) .align(f.a.r, "NE")) ================================================ FILE: examples/image_rotated_quality.py ================================================ from coldtype import * from coldtype.raster import * @animation(bg=1) def scratch(f): shape = StSt("ABC", Font.MuSan(), 300, wght=1).align(f.a.r) if 1: img = shape.ch(precompose(f.a.r)).img()["src"] img = SkiaImage(img).rotate(36).ch(precompose(f.a.r)) for _ in range(0, 9*31): img = SkiaImage(img.img()["src"]).rotate(36).ch(precompose(f.a.r)) else: img = shape return img ================================================ FILE: examples/instancer.py ================================================ from coldtype import * src = Font.JetBrainsMono() @renderable((1080, 540), bg=1) def viewer(r): txt = "“Hello World” øö ĦĦĦ 123" textface = StyledString(txt, Style(src, 62, wght=1, ss02=1)) instance = textface.instance(ººsiblingºº("JBMono-custom.otf"), freeze=1, freeze_suffix="Custom") subsetted = instance.subset(ººsiblingºº("JBMono-custom-subset.woff2")) return (P( textface.pens(), StSt(txt, instance, 62), StSt(txt, subsetted, 62), ) .f(0) .stack(10) .align(r)) ================================================ FILE: examples/interpolated_spiral.py ================================================ from coldtype import * rs = random_series() @aframe() def spiral(f): return (P().enumerate(range(0, 30), lambda x: StSt("COLDTYPE", Font.ColdtypeObviously(), 220, wdth=x.e, ro=1) .align(f.a.r) .fssw(-1, hsl(x.e, s=0.7, a=1.3-x.e), 2) .rotate(-200+x.e*200))) ================================================ FILE: examples/interrupted_lines.py ================================================ from coldtype import * def interrupt_lines(lines:list[Line], rects:list[Rect]): out = P() for l in lines: p = P().m(l.start) for rect in rects: ra = rect.ambit() if l.intersects(ra.ew): p1 = l.intersection(ra.ew) p2 = l.intersection(ra.ee) p.l(p1).ep() p.m(p2) out += p.l(l.end).ep() return out @aframe(bg=1) def lines(f:Frame): s = Scaffold(f.a.r).numeric_grid(10, 10) rects = (P( P(s["3|3"].r.inset(0, -14)), P(s["6|4*2|2"].r.inset(0, 14)) )).fssw(-1, hsl(0.6), 1) lines = [] for x in range(1, 10): start, end = s[f"0|{x}"], s[f"9|{x}"] lines.append(Line(start.r.psw, end.r.pse)) pts = interrupt_lines(lines, [r.ambit().inset(-8, 0) for r in rects]) return rects + pts.fssw(-1, hsl(0.8), 1) ================================================ FILE: examples/layers.py ================================================ from coldtype import * @renderable((500, 500)) def layer1(r): return P().oval(r.inset(10)).f(hsl(0.3)) @renderable((500, 500), layer=1) def layer2(r): return P().rect(r.inset(150)).rotate(45).f(hsl(0.6)) ================================================ FILE: examples/letter_lighttrail.py ================================================ from coldtype import * from coldtype.raster import * r = Rect(1080, 1080) letters = "RANDOM" rc = random_series() rstroke = random_series(0.45, 1) @animation(r, bg=1 , tl=Timeline(240, 12) , release=lambda x: x.export("h264", loops=2, date=True)) def scribble_random(f): seed = f.i+2 letter = (StSt( letters[math.floor(f.e("l", 0, rng=(0, len(letters) )))] , font=Font.MuSan() , fontSize=1105 , wght=f.e("l", 1) , wdth=0.25 , ro=1) .align(r, ty=1) .scaleToRect(f.a.r.inset(50))) points = letter.pen().flatten(10).point_list() rp = random_series(0, len(points), seed=seed, mod=int) lines = (P().enumerate(range(0, int(f.e("eei", 1, rng=(15, 25)))), lambda x: P() .moveTo(points[rp[x.i]]) .enumerate(range(0, int(f.e("seio", rng=(6, 200)))), lambda y: y.parent .declare(yi:=y.i*10) .cond(rc[x.i+yi] > f.e("l", rng=(0.1, 0.75)), lambda p: p.lineTo(points[rp[1+yi+x.i]]), lambda p: p.curveTo( points[rp[1+yi+x.i]], points[rp[2+yi+x.i]], points[rp[3+yi+x.i]]))) .endPath() .fssw(-1, 1, rstroke[x.i]) .ch(phototype(f.a.r , blur=2 , cut=40 , cutw=15 , fill=hsl(x.e*0.5+f.e("l", 0, rng=(0, 10)), 0.9, 0.4)))) .blendmode(BlendMode.Cycle(16))) return (P( P(f.a.r) .ch(spackle(cut=235, cutw=1, base=f.i, fill=bw(1))) .ch(phototype(f.a.r, blur=3, cut=170, fill=hsl(f.i*0.1))), lines) .postprocess(lambda p: p.ch(temptone(.05,.02)))) ================================================ FILE: examples/linealigning.py ================================================ from coldtype import * @renderable((1080, 540), bg=1) def aligns(r): ri = r.inset(100) return (P( P(r.inset(50)).fssw(-1, 0, 1), StSt("Coldest\nType".upper(), Font.MuSan(), fs:=72).xalign(ri, "W").align(ri, "NW"), StSt("Coldest\nType".upper(), Font.MuSan(), fs).xalign(ri).align(ri), StSt("Coldest\nType".upper(), Font.MuSan(), fs).xalign(ri, "E").align(ri, "SE"), )) ================================================ FILE: examples/linebreaking.py ================================================ from coldtype import * txt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquet neque, non bibendum nisi. Mauris quis ligula felis." @renderable(bg=1) def scratch(r): ri = r.inset(50) return (StSt(txt, Font.JBMono(), 70) .wordPens() .linebreak(ri.w) .stack("100%") .align(ri) .f(0)) @renderable(bg=1) def heterogenous(r): ri = r.inset(50) a = StSt("Hello", Font.JBMono(), 70, wght=1) b = StSt("TYPE", Font.ColdObvi(), 70, wght=1) c = StSt("WORLD", Font.MuSan(), 70, wght=1) return (P(a, b, c) .spread(40) .linebreak(500) .stack(20) .align(ri)) ================================================ FILE: examples/logo.py ================================================ from coldtype import * from coldtype.fx.skia import phototype """ For instagram """ @animation(bg=hsl(0.65, 0.6, 0.45), solo=1, tl=2) def logo(f): circle_guide = P().oval(f.a.r.inset(-20)).fssw(-1, 1, 2) if f.i == 0: return P( circle_guide, StSt("COLD\nTYPE", Font.ColdObvi(), 500 , wdth=0.5 , tu=-50 , kp={"P/E":-80} , leading=-10) .index(0, lambda p: p.translate(-130, 0)) .reverse(recursive=1) .align(f.a.r, tx=1, ty=1) .rotate(15) .translate(-3, 3) .fssw(1, 0, 25, 1) .ch(phototype(f.a.r, blur=5, cut=170, cutw=11, fill=bw(1)))) else: return P( circle_guide, StSt("C", Font.ColdObvi(), 850, wdth=1) .align(f.a.r) .f(1) .rotate(10) .translate(-5, -7) .ch(phototype(f.a.r, blur=5, cut=150, cutw=15, fill=bw(1)))) ================================================ FILE: examples/logo_state.json ================================================ {"controller_values": {"Launch Control XL": {"78": 0.5905511811023622, "77": 0.36220472440944884, "79": 0.5275590551181102, "80": 0.47244094488188976, "81": 0.6299212598425197}}} ================================================ FILE: examples/metaprogramming.py ================================================ from coldtype import * from coldtype.fx.warping import warp from coldtype.text.richtext import PythonCode ### Code that displays itself styles = ["R", "Bl", "B.*I"] vulfs = [Font.Find(f"Mono{s}", "vulf") for s in styles] defaults = [vulfs[0], hsl(0.65, 0.6, 0.65)] lookup = PythonCode.DefaultStyles(*vulfs) rnds1 = random_series() @aframe(bg=0) def code1(f): def render_code(txt, styles): style = styles[0] if len(styles) > 0 else "default" font, fill = lookup.get(style, defaults) return txt, Style(font, 22, fill=fill) ri = f.a.r.inset(20) return (PythonCode(ri , Path(__FILE__).read_text()[:] , render_code , leading=8) .align(ri, "W") .removeSpacers() .collapse() .pmap(lambda i, p: p.rotate(-15+rnds1[i]*30)) .pmap(warp(2, 0, 0, mult=30))) ================================================ FILE: examples/mirror.py ================================================ from coldtype import * @renderable() def mirror(r): return (StSt("C", Font.ColdObvi(), 300) .align(r.inset(100).take(0.5, "NW")) .mirrorx(r.pc) .mirrory(r.pc) .layer( lambda p: p.rotate(45, point=r.pc) .f(hsl(0.9, 0.8)), lambda p: p.f(hsl(0.65, 0.8)))) ================================================ FILE: examples/misc/no_command_line.py ================================================ from coldtype.renderer import Renderer """ `python examples/misc/no_command_line.py` An example of how to call the Coldtype renderer directly, without the need to use the command line at all (mostly useful if you’re batch-rendering fully written animations) That said, if you take out the "-a" arg, this should run exactly like the normal coldtype viewer — potentially useful for a situation in which it’s difficult to access the `coldtype` bin command, since you could run this with just the standard `python` bin command """ _, parser = Renderer.Argparser() parsed = parser.parse_args(["examples/animations/simplevarfont.py", "-a"]) Renderer(parsed).main() ================================================ FILE: examples/opentypesvgimagefont.py ================================================ from coldtype import * from coldtype.raster import text_image fnt, fs = "California Oranges", 200 fnt, fs = "Liebeheide", 100 fnt, fs = "PinkSugar", 500 @renderable((1080, 540), bg=1) def texter(r): return (StSt("OTSVG", fnt, fs, annotate=1) .align(r, ty=1) .layer( lambda p: p.align(r, "N", ty=1).ch(text_image(r)), lambda p: p.align(r, "S", ty=1).ch(text_image(r)), lambda p: p.mapv(lambda p: p.ch(text_image(r))), lambda p: p.fssw(-1, rgb(1, 1, 1), 2))) ================================================ FILE: examples/potracer.py ================================================ from coldtype import * from coldtype.raster import phototype, potrace @renderable((1080, 540), bg=1) def potraced(r): return (StSt("COLD".upper(), Font.ColdObvi(), 300, wdth=1, tu=-190) .align(r) .reverse() .fssw(1, 0, 10, 1) .ch(phototype(r, 3, cut=170, cutw=16)) .ch(potrace(r, turdsize=100)) #.print(lambda p: p.value) .f(hsl(0.8))) ================================================ FILE: examples/printer.py ================================================ from coldtype import * txt = """ 1 Print 2 Photograph 3 Crumple 4 goto 2 """ @renderable("letter-landscape", bg=1, render_bg=0, fmt="pdf") def printed(r): return (StSt(txt, "polymathvar.ttf", 72, space=300, tu=0, opsz=1, case="lower", leading=34, tnum=1, wght=0.65) .align(r) .f(0)) def release(_): import cups path = printed.render_to_disk()[0] conn = cups.Connection() printers = conn.getPrinters() print(printers) conn.printFile(conn.getDefault(), str(path), printed.name, {"orientation-requested": "4"}) import cups ================================================ FILE: examples/random_shape.py ================================================ from coldtype import * from coldtype.fx.skia import phototype @animation(bg=0, tl=Timeline(120, 12)) def scribble(f): seed = f.i ri = f.a.r.inset(50) rx = random_series(ri.mnx, ri.mxx, seed=seed) ry = random_series(ri.mny, ri.mxy, seed=seed+1) return (P().enumerate(range(0, int(f.e("seio", 1, rng=(1, 200)))), lambda x: P() .moveTo(rx[x.i], ry[x.i]) .enumerate(range(0, 20), lambda y: y.parent .declare(yi:=y.i*10) .curveTo( (rx[1+yi+x.i], ry[1+yi+x.i]), (rx[2+yi+x.i], ry[2+yi+x.i]), (rx[3+yi+x.i], ry[3+yi+x.i]))) .endPath() .fssw(-1, 1, f.e("eei", 1, rng=(0.5, 5))) .ch(phototype(f.a.r , blur=f.e("eei", 1, rng=(1.75, 10)) , cut=40 , cutw=15 , fill=hsl(x.e*0.5+f.e("l", 0, rng=(0, 10)), 0.8)))) .blendmode(BlendMode.Cycle(14))) release = scribble.export("h264", loops=2) ================================================ FILE: examples/restmake.py ================================================ from coldtype import * from coldtype.raster import * from functools import partial rsx = random_series(-2, 2, 1) rsy = random_series(-2, 2, 2) rsa = random_series(-10, 10, 3) rsw = random_series(0, 1, 4) @renderable(bg=hsl(0.7)) def restmake(r): d = 17 s = Scaffold(r.inset(0, 60)).numeric_grid(d) def letter(txt, i, r, seed=0): return (StSt(list(txt)[i%4], "NCND", 40, wght=rsw[i+seed]) .pen() .unframe() .f(0) .align(r) .t(rsx[i+seed*5], rsy[i+seed*10]) .rotate(rsa[i+seed])) def point(txt, seed, x): row = x.el.data("row") col = x.el.data("col") if p := x.el.r.pc if row%2 == 0 else x.el.r.pe if col < d - 1 else None: return letter(txt, x.i, Rect.FromCenter(p, 20), seed).f(1) return (P( P().enumerate(s.cells(), partial(point, "rest", 0)), P().enumerate(s.cells(), partial(point, "make", 1)) .rotate(10, point=s.r.psw), ).align(s).ch(phototype(r, 1, 110, 65))) ================================================ FILE: examples/richtext.py ================================================ from coldtype import * from coldtype.text.richtext import RichText def styler(txt, styles): if "i" in styles: return txt, Style(Font.RecursiveMono(), 22) elif "h" in styles: return txt, Style(Font.MutatorSans(), 72, wght=1) return txt, Style(Font.RecursiveMono(), 42) @renderable((1080, 200)) def highlight(r): return (RichText(r, "HELLO[h] World", styler) .xalign(r) .align(r, ty=1) .scale(1.5) .insert(0, lambda ps: P() .rect(ps[0][-1] .ambit(tx=1, ty=1) .inset(-10)) .fssw(-1, hsl(0.7, a=0.3), 10))) txt = """H [h] Text a smaller line [i]""" @renderable((1080, 540)) def plainish(r): return (RichText(r, txt, styler, spacer="¶") .xalign(r) .align(r) .scale(2, tx=1) .f(0) .removeSpacers()) txt2 = """ Hello, [a] WORLD """ @renderable((1080, 400)) def key_lookup_style(r): return (RichText( r, txt2, { "a": Style(Font.RecursiveMono(), 200, fill=hsl(0.9)), "default": Style(Font.MuSan(), 150, fill=bw(0), wght=1)}) .xalign(r) .reversePens() .lead(20) .align(r)) ================================================ FILE: examples/rounded_corners.py ================================================ from coldtype import * from coldtype.fx.skia import round_corners """ Use a skia effect to apply round corners to a sharp-edged shape """ @aframe((1080, 540), bg=1) def rounder(f): return (P(Rect(400)) .align(f.a.r) .fssw(-1, 0, 2) .pen() .remove_overlap() .ch(round_corners(60))) ================================================ FILE: examples/scaffold.py ================================================ from coldtype import * @renderable() def boxes(r): l = (Scaffold(r) .cssgrid(r"auto 30%", r"50% auto", "x y / z q", { "x": ("200 a", "a a", "a b / a c"), "x/c": ("a a", "a a", "g a / i a"), "q": ("a a", "a a", "q b / c d") })) return P( l.view(), StSt("X", Font.MuSan(), 200, wght=1).align(l["x"]), StSt("Y", Font.MuSan(), 200, wght=1).align(l["y"]), StSt("Z", Font.MuSan(), 200, wght=1).align(l["z"]), StSt("Q", Font.MuSan(), 200, wght=1).align(l["q"]), StSt("A", Font.RecMono(), 100).align(l["a"]).f(hsl(0.9)), StSt("B", Font.RecMono(), 100).align(l["b"]).f(hsl(0.9)), StSt("C", Font.RecMono(), 100).align(l["c"]).f(hsl(0.9)), StSt("G", "Comic Sans", 50).align(l["g"]).f(0), StSt("A", "Comic Sans", 50).align(l["x/c/a"]).f(0), StSt("I", "Comic Sans", 50).align(l["i"]).f(0), P().oval(l["x/c/a"]).fssw(-1, hsl(0.3, a=0.5), 10), StSt("Q/Q", "Comic Sans", 50).align(l["q/q"]).f(0),) @renderable((1080, 540)) def boxes2(r): l = Scaffold(r).grid(2, 2) return P( l.view(), P().oval(l[0].rect.square().inset(10)).fssw(-1, hsl(0.9, 1), 2)) @renderable((1080, 540)) def boxes3(r): l = Scaffold(r).numeric_grid(5) return P( l.view(), P().oval(l["3|1"].rect.square().inset(10)).fssw(-1, hsl(0.9, 1), 2) ) @renderable((1080, 540), solo=0) def boxes4(r): d = 15 l = Scaffold(r.inset(4).square()).numeric_grid(d, gap=1, annotate_rings=True) return (P().enumerate(l.cells(), lambda x: P(x.el.r) .f(hsl(x.el.data("ring_e")*2, 0.6, 0.6)) .tag(x.el.tag())) .find_("3|12", lambda p: p.f(0)) .find_("11|2", lambda p: p.f(1))) ================================================ FILE: examples/scripts/player.py ================================================ from coldtype import * from coldtype.renderable.animation import image_sequence, shutil def sorted_images(folder, suffix="*.jpg"): return list(sorted(folder.glob(suffix), key=lambda p: p.name)) folder = Path(__inputs__[0]) fps = int(__inputs__[1]) images = sorted_images(folder) parent = folder.parent base_name = parent.name + "_" + folder.name name = base_name @image_sequence(images, fps, looping=True, name=name) def viewer(_): return None def release(_): FFMPEGExport(viewer, loops=1, output_folder=parent).prores().write(True) if True: shutil.rmtree(viewer.output_folder) ================================================ FILE: examples/scripts/prores.py ================================================ from coldtype import * from coldtype.renderable.animation import image_sequence, shutil def sorted_images(folder, suffix="*.jpg"): return list(sorted(folder.glob(suffix), key=lambda p: p.name)) folder = Path(__inputs__[0]) images = sorted_images(folder) images_hires = None for img in images: print(img) if (folder / "hires").exists(): images_hires = sorted_images(folder / "hires") parent = folder.parent base_name = parent.name + "_" + folder.name name = base_name fps = 12 if images_hires: name = base_name + "_proxy" @image_sequence(images_hires, fps, looping=True, name=base_name, render_only=True) def viewer_hires(_): return None @image_sequence(images, fps, looping=False, name=name) def viewer(_): return None def release(_): FFMPEGExport(viewer, loops=1, output_folder=parent).prores().write(True) if images_hires: FFMPEGExport(viewer_hires, loops=1, output_folder=parent).prores().write(True) if True: # clean up unnecessary files shutil.rmtree(viewer.output_folder) if images_hires: shutil.rmtree(viewer_hires.output_folder) ================================================ FILE: examples/scripts/prores_to_frames.py ================================================ import cv2 from coldtype import * from coldtype.img.skiaimage import SkiaImage mov_path = Path(ººinputsºº[0]).expanduser() print(mov_path.exists()) vc = cv2.VideoCapture(mov_path) r = Rect(int(vc.get(cv2.CAP_PROP_FRAME_WIDTH)), int(vc.get(cv2.CAP_PROP_FRAME_HEIGHT))) frame_count = int(vc.get(cv2.CAP_PROP_FRAME_COUNT)) - 1 frames_dir = (mov_path.parent / f"{mov_path.name}__frames") frames_dir.mkdir(exist_ok=True, parents=True) print(frame_count) @animation(r, tl=Timeline(frame_count)) def prores(f): vc.set(cv2.CAP_PROP_POS_FRAMES, f.i) _, frame = vc.read() cv2.imwrite(frames_dir / "{:05d}.jpg".format(f.i), frame) return P(SkiaImage(frames_dir / "{:05d}.jpg".format(f.i)), StSt(str(f.i), Font.JetBrainsMono(), 100).f(0).align(f.a.r, tx=0)) ================================================ FILE: examples/scripts/symbolfinder.py ================================================ from coldtype import * fonts = Font.LibraryList(r".*") symbol = "♔" found = False print(Font.LibraryFind("Arial Unicode.ttf")) @animation((540, 540), tl=Timeline(len(fonts), 60), bg=0) def finder(f): global found found = False print(fonts[f.i]) font = Font.LibraryFind(fonts[f.i]) print(font) try: res = (StSt(symbol, font, 500) .align(f.a.r) .f(1)) if res[0].glyphName not in [".notdef", "uni0019", "uni0001", "glyph0", "uni0000"]: found = True print(">>>"*30) print(f.i, f.t.duration, font.path.name, res[0].glyphName) print("---"*30) print(font.path) print("---"*30) return res.data(sleep=3) except: pass return StSt(str(f.i), Font.JBMono(), 20).f(1).align(f.a.r, tx=0) def didPreview(): from time import sleep if found: sleep(2) ================================================ FILE: examples/shapes.py ================================================ from coldtype import * # a coldtypification of https://github.com/djrrb/Python-for-Visual-Designers-Fall-2023/blob/main/session-4/code/2-BezierPathLoop.py seed = 1 wobble = 250 handle_length = 100 rw1 = random_series(-wobble, wobble, seed=seed) rw2 = random_series(-wobble, wobble, seed=seed+1) rc = random_series(seed=seed+2) @renderable(bg=1) def shapes(r): def build_shape(x): return (P() .moveTo((0, 0)) .lineTo((r.w, 0)) .lineTo((r.w, h:=r.h*(1-x.e))) .curveTo( (r.w, h+handle_length+rw1[x.i]), (0, h+handle_length+rw2[x.i]), (0, h)) .closePath() .f(hsl(rc[x.i], 0.6, 0.6, 0.5))) return (P().enumerate(range(0, 10), build_shape) .scale(0.75, pt=(0, 0)) .xalign(r, "CX")) ================================================ FILE: examples/simple.py ================================================ from coldtype import * @renderable((1580, 350)) def render(r): return P( P(r.inset(10)).outline(10).f(hsl(0.65)), StSt("COLDTYPE", Font.ColdtypeObviously() , fontSize=250 , wdth=1 , tu=-250 , r=1 , rotate=15) .align(r) .fssw(hsl(0.65), 1, 5, 1) .translate(0, 5)) ================================================ FILE: examples/simplest.py ================================================ from coldtype import * @renderable(rect=(1200, 340), bg=0) def render(r): return (StSt("T", Font.MuSan(), 200) .align(r) .f(1)) ================================================ FILE: examples/sites/.gitignore ================================================ *.woff2 ================================================ FILE: examples/sites/blog.coldtype.xyz/.gitignore ================================================ ================================================ FILE: examples/sites/blog.coldtype.xyz/assets/style.css ================================================ html { box-sizing: border-box; font-size: 16px; font-family: var(--text-font), sans-serif; } pre, code, pre span, code span { font-family: var(--mono-font), monospace !important; } *, *:before, *:after { box-sizing: inherit; } ol, ul { list-style: none; } img { max-width: 100%; height: auto; } body { background: white; } header { font-size: 1rem; margin-bottom: 100px; padding-top: 20px; } header a { color: dodgerblue; text-decoration: none; } header a:hover { color: white; background-color: dodgerblue; } header h1 { font-size: 1.75em; --mono-font: fvs(wght=1); } nav ul { list-style: none; display: flex; flex-wrap: wrap; margin-top: 16px; justify-content: center; row-gap: 12px; } nav li { /* display: flex; */ } nav li:not(:last-child)::after { content: "///"; color: #ccc; } nav li a { padding: 4px 7px; background: whitesmoke; } nav li a:hover { background: deeppink; } .wrapper { max-width: 650px; margin: auto auto; padding: 20px; } .post-link a { display: block; text-decoration: none; color: dodgerblue; /* padding: 10px; */ border-left: 2px solid dodgerblue; } .post-link a h2 { padding: 2px 5px 2px 10px; font-family: var(--text-font); font-variation-settings: "wght" 800, "wdth" 60; } .post-link a:hover h2 { background-color: dodgerblue; color: white; } .post-link a h2 span.date { color: #ccc; font-variation-settings: "wght" 300, "wdth" 50; } .post-link a h2 span.grocer { color: #ccc; display: none; } .colab-callout { margin-bottom: 100px; } a { color: dodgerblue; } a.colab-link { --mono-font: fvs(ital=1, wght=0.65); color: dodgerblue; text-align: left; opacity: 0.65; text-decoration: none; } a.colab-link:hover { opacity: 1; } .post .headline { margin-bottom: 90px; } .post { margin-bottom: 50px; /* padding: 20px; */ } .post p { margin-bottom: 14px; font-size: 1.1rem; line-height: 150%; } .post h1 { --mono-font: fvs(wght=0.85); margin-top: 30px; margin-bottom: 10px; } .post h2 { --mono-font: fvs(wght=0.85); margin-top: 50px; margin-bottom: 30px; } .post h3 { --mono-font: fvs(wght=0.75); margin-top: 30px; margin-bottom: 20px; } .post ul { margin-bottom: 14px; list-style: circle; padding-left: 20px; } .post li { font-size: 1.1rem; line-height: 1.4rem;; margin-bottom: 10px; } .post code { --mono-font: fvs(wght=0.35); color: hsl(320, 80%, 50%); } .post strong { --text-font: fvs(wght=0.85); } .post em { --text-font: fvs(slnt=0); font-style: normal; } hr { border: none; border-bottom: 1px solid #eee; margin-top: 40px; margin-bottom: 40px; } /* .post iframe { margin: 50px 0; } */ .post .author { margin-top: 30px; text-align: right; font-style: italic; color: #555; margin-bottom: 200px; } .code.no-outputs { margin-bottom: 30px; border-bottom: 1px solid #ccc; } .code-outputs { margin-bottom: 60px; border: 1px solid #ccc; border-top: none; padding: 8px; } .code-outputs img { display: block; margin-bottom: 8px; } .code-outputs img:last-child { margin-bottom: 0; } /* .post pre { display: block; overflow: scroll; padding: 10px 0; margin: 25px 0; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; } */ .text { margin-bottom: 40px; } .text pre { margin-bottom: 20px; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; padding: 10px 0; overflow-x: scroll; } .highlight pre { /* font-family: covik-sans-mono, monospace; */ padding: 12px 10px; /* border-top: 1px solid #ccc; */ /* border-bottom: 1px solid #ccc; */ font-size: 1rem; overflow-x: scroll; opacity: 1; height: auto; margin-bottom: 0px; background: whitesmoke; border: 1px solid #ccc; border-bottom: none; } .highlight .hll { background-color: #ffffcc } .highlight .c { color: #0099FF; --mono-font:fvs(ital=1, wght=0.5); } /* Comment */ .highlight .err { color: #AA0000; background-color: #FFAAAA } /* Error */ .highlight .k { color: #006699; --mono-font:fvs(wght=0.65); } /* Keyword */ .highlight .n { color: #A02299; } /* Keyword */ .highlight .o { color: #555555 } /* Operator */ .highlight .cm { color: #0099FF; --mono-font:fvs(ital=1, wght=0.5); } /* Comment.Multiline */ .highlight .cp { color: #009999 } /* Comment.Preproc */ .highlight .c1 { color: #0099FF; --mono-font:fvs(ital=1, wght=0.5); } /* Comment.Single */ .highlight .cs { color: #0099FF; --mono-font:fvs(ital=1, wght=0.65); } /* Comment.Special */ .highlight .gd { background-color: #FFCCCC; border: 1px solid #CC0000 } /* Generic.Deleted */ .highlight .ge { font-style: italic } /* Generic.Emph */ .highlight .gr { color: #FF0000 } /* Generic.Error */ .highlight .gh { color: #003300; --mono-font:fvs(wght=0.65); } /* Generic.Heading */ .highlight .gi { background-color: #CCFFCC; border: 1px solid #00CC00 } /* Generic.Inserted */ .highlight .go { color: #AAAAAA } /* Generic.Output */ .highlight .gp { color: #000099; --mono-font:fvs(wght=0.65); } /* Generic.Prompt */ .highlight .gs { font-weight: bold } /* Generic.Strong */ .highlight .gu { color: #003300; --mono-font:fvs(wght=0.65); } /* Generic.Subheading */ .highlight .gt { color: #99CC66 } /* Generic.Traceback */ .highlight .kc { color: #006699; --mono-font:fvs(wght=0.65); } /* Keyword.Constant */ .highlight .kd { color: #006699; --mono-font:fvs(wght=0.65); } /* Keyword.Declaration */ .highlight .kn { color: #006699; --mono-font:fvs(wght=0.65); } /* Keyword.Namespace */ .highlight .kp { color: #006699 } /* Keyword.Pseudo */ .highlight .kr { color: #006699; --mono-font:fvs(wght=0.65); } /* Keyword.Reserved */ .highlight .kt { color: #007788; --mono-font:fvs(wght=0.65); } /* Keyword.Type */ .highlight .m { color: #FF6600 } /* Literal.Number */ .highlight .s { color: #CC3300 } /* Literal.String */ .highlight .na { color: #330099 } /* Name.Attribute */ .highlight .nb { color: #336666 } /* Name.Builtin */ .highlight .nc { color: #00AA88; --mono-font:fvs(wght=0.65); } /* Name.Class */ .highlight .no { color: #336600 } /* Name.Constant */ .highlight .nd { color: #9999FF } /* Name.Decorator */ .highlight .ni { color: #999999; --mono-font:fvs(wght=0.65); } /* Name.Entity */ .highlight .ne { color: #CC0000; --mono-font:fvs(wght=0.65); } /* Name.Exception */ .highlight .nf { color: #CC00FF } /* Name.Function */ .highlight .nl { color: #9999FF } /* Name.Label */ .highlight .nn { color: #00CCFF; --mono-font:fvs(wght=0.65); } /* Name.Namespace */ .highlight .nt { color: #330099; --mono-font:fvs(wght=0.65); } /* Name.Tag */ .highlight .nv { color: #003333 } /* Name.Variable */ .highlight .ow { color: #000000; --mono-font:fvs(wght=0.65); } /* Operator.Word */ .highlight .w { color: #bbbbbb } /* Text.Whitespace */ .highlight .mf { color: #FF6600 } /* Literal.Number.Float */ .highlight .mh { color: #FF6600 } /* Literal.Number.Hex */ .highlight .mi { color: #FF6600 } /* Literal.Number.Integer */ .highlight .mo { color: #FF6600 } /* Literal.Number.Oct */ .highlight .sb { color: #CC3300 } /* Literal.String.Backtick */ .highlight .sc { color: #CC3300 } /* Literal.String.Char */ .highlight .sd { color: #CC3300; --mono-font:fvs(ital=1, wght=0.65) } /* Literal.String.Doc */ .highlight .s2 { color: #CC3300 } /* Literal.String.Double */ .highlight .se { color: #CC3300; --mono-font:fvs(wght=0.65); } /* Literal.String.Escape */ .highlight .sh { color: #CC3300 } /* Literal.String.Heredoc */ .highlight .si { color: #AA0000 } /* Literal.String.Interpol */ .highlight .sx { color: #CC3300 } /* Literal.String.Other */ .highlight .sr { color: #33AAAA } /* Literal.String.Regex */ .highlight .s1 { color: #CC3300 } /* Literal.String.Single */ .highlight .ss { color: #FFCC33 } /* Literal.String.Symbol */ .highlight .bp { color: #336666 } /* Name.Builtin.Pseudo */ .highlight .vc { color: #003333 } /* Name.Variable.Class */ .highlight .vg { color: #003333 } /* Name.Variable.Global */ .highlight .vi { color: #003333 } /* Name.Variable.Instance */ .highlight .il { color: #FF6600 } /* Literal.Number.Integer.Long */ .highlight .p { color: hsl(250, 60%, 60%); } ================================================ FILE: examples/sites/blog.coldtype.xyz/blog.coldtype.xyz.py ================================================ from coldtype import * from coldtype.web.site import * def public_posts(site): return list(reversed(sorted([p for p in site.pages if p.template == "_post"], key=lambda p: p.date))) info = dict( title="The Coldtype Blog", description="This is the Coldtype blog", navigation={ "Home": "/", #"About": "/about" }) @site(ººsiblingºº(".") , port=8008 , sources=dict(public_posts=public_posts) , info=info , fonts={ "text-font": dict(regular="MDSystem-VF"), "mono-font": dict(regular="MDIO-VF")}) def website(_): website.build() def release(_): website.upload("blog.coldtype.xyz", "us-west-1", "personal") ================================================ FILE: examples/sites/blog.coldtype.xyz/pages/posts/a-blog.ipynb ================================================ {"cells":[{"cell_type":"code","execution_count":2,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":193,"status":"ok","timestamp":1706210528223,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"P8XWe6RmY7d2","outputId":"97461174-01fa-43bc-d94b-d1d97ebec077"},"outputs":[{"data":{"text/plain":["{'author': 'Rob Stenson',\n"," 'location': 'Los Angeles, California',\n"," 'slug': 'a-blog',\n"," 'title': 'A Blog!',\n"," 'date': '01/4/2021',\n"," 'description': 'Introducing the Coldtype blog'}"]},"execution_count":2,"metadata":{},"output_type":"execute_result"}],"source":["dict(\n"," author=\"Rob Stenson\",\n"," location=\"Los Angeles, California\",\n"," slug=\"a-blog\",\n"," title=\"A Blog!\",\n"," date=\"01/4/2021\",\n"," description=\"Introducing the Coldtype blog\"\n",")"]},{"cell_type":"code","execution_count":null,"metadata":{"id":"RqIIr_Q4Y_L9"},"outputs":[],"source":["#hide-blog\n","%pip install -q \"coldtype[notebook] @ git+https://github.com/goodhertz/coldtype\"\n","%pip install -q noise\n","from coldtype.notebook import *"]},{"cell_type":"markdown","metadata":{"id":"4U-rNEQwfARg"},"source":["Welcome to the Coldtype blog.\n","\n","You may be wondering why an open-source typography/graphics library needs a blog, and the answer is… I thought it would be fun to write some un-structured stuff about this open-source typography/graphics library.\n","\n","At this moment in the library’s existence, though I know there are some people out there trying it out, I’m the only person that has used Coldtype extensively. Consequently, the library and its features are very closely related to my own interests. Well, that’s kind of obvious I guess — what I mean is, more generally, there are a ton of bizarre features in Coldtype (particularly related to animation) that come from my own experiences attempting to combine commercial animation software with Python code, or my own experiences trying to combine commercial type design software with Python code, or my experiences trying to design commercial audio plugins with Python code.\n","\n","So the plan for this blog is to shed some light on the motivation behind those bizarre features — and even if no one reads it, it’ll be a nice thing for me to read in the future and think, _oh, right, that’s why I did that_.\n","\n","_(Revised 11/14/2022)_"]},{"cell_type":"code","execution_count":5,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":121},"executionInfo":{"elapsed":232,"status":"ok","timestamp":1668451881788,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"NpQirv6FZLCm","outputId":"9aa27ad4-f367-4169-8ee9-0cf8da1d1a0c"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"}],"source":["from coldtype.notebook import *\n","from coldtype.warping import warp\n","from coldtype.fx.skia import phototype\n","\n","@renderable((600, 200))\n","def header(r):\n"," return (StSt(\"A BLOG\", Font.MutatorSans(), 150, wght=0.75, tu=-150, r=1)\n"," .f(1)\n"," .align(r, tx=1, ty=1)\n"," .understroke(sw=20)\n"," .mapv(warp())\n"," .ch(phototype(r, blur=3, cut=150, cutw=20\n"," , fill=hsl(0.6, 1, 0.55))))"]}],"metadata":{"colab":{"authorship_tag":"ABX9TyMTvc4wP2WgHf+GvY9sm5LY","provenance":[]},"kernelspec":{"display_name":"Python 3","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.7"}},"nbformat":4,"nbformat_minor":0} ================================================ FILE: examples/sites/blog.coldtype.xyz/pages/posts/transparent-unclickable.ipynb ================================================ {"cells":[{"cell_type":"code","execution_count":2,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":262,"status":"ok","timestamp":1706210596657,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"Hj35zB-CgNP6","outputId":"5381b48f-9ee2-4592-ae30-57fc7d6c702f"},"outputs":[{"data":{"text/plain":["{'author': 'Rob Stenson',\n"," 'location': 'Los Angeles, California',\n"," 'slug': 'transparent-unclickable',\n"," 'title': 'A Transparent & Unclickable Window',\n"," 'date': '05/21/2021',\n"," 'description': 'Getting the settings right for special windowing in Coldtype'}"]},"execution_count":2,"metadata":{},"output_type":"execute_result"}],"source":["dict(\n"," author=\"Rob Stenson\",\n"," location=\"Los Angeles, California\",\n"," slug=\"transparent-unclickable\",\n"," title=\"A Transparent & Unclickable Window\",\n"," date=\"05/21/2021\",\n"," description=\"Getting the settings right for special windowing in Coldtype\"\n",")"]},{"cell_type":"markdown","metadata":{"id":"RNXg-EmNgjSc"},"source":["One thing that's very fun, particularly on smaller screens, is to run a Coldtype viewer window with a transparent background directly on top of some code that you’re working on. That’s exactly what’s happening in the video below — it’s not a composite of a video and some code, it’s just literally what appears on the screen as I program.\n","\n","

a little bit of realtime python
& a realtime animation

13 lines of coldtype + @ohnotypeco’s latest ttf pic.twitter.com/2BRClOjore

— Rob Stenson (@robstenson) May 21, 2021
\n","\n","To get something like that to work, there are two steps, because there are actually two different things happening: (1) the window is transparent (this is easy), and (2) the window ignores all mouse interaction (not as easy).\n","\n","You can see the correct invocation for both at the beginning of that video, but here it is again:\n","\n","```bash\n","coldtype examples/animations/simplevarfont.py -wt 1 -wp NW -wpass 1 -wc 1\n","```\n","\n","- **-wt** stands for `--window-transparent` — this should work right out of the box\n","- **-wc** stands for `--window-chromeless` — this should also work straight away\n","- **-wp NW** means `--window-pin` is set to `NW`, meaning the window should initially appear at the top-left of the monitor (this can have any compass value (i.e. N, S, SW, SE, etc.) or C for Center).\n","- **-wpass** is the tricky one — it means `window-passthrough` and relies on a feature of the glfw library that hasn't made it to the stable release yet.\n","\n","\n","## The unclickable window\n","\n","We’re leaving the main road here and headed onto dirt. Unfortunately, I don't know how to do this on Windows, so these instructions are just for Mac OS.\n","\n","All you need to do (\"all you need to do\") is install the \"head\" of glfw, via [brew](https://brew.sh/).\n","\n","```bash\n","brew install glfw --HEAD\n","```\n","\n","Now the -wpass argument should work.\n","\n","## A semi-transparent window\n","\n","One other thing you can do is set the opacity of the window itself to something other than the usual fully-opaque. I probably should’ve done this in the video above, where I briefly made my own code invisible because of the blue text overlaying it. I could’ve avoided that by adding `-wo 0.5` to the command-line invocation, which would’ve cut the transparency of the whole window in half, meaning even a full-alpha color in the rendered viewer would've been transparent (and I could’ve seen all of the code)."]}],"metadata":{"colab":{"authorship_tag":"ABX9TyO6/e2bZXquocbzYquVM3dn","provenance":[]},"kernelspec":{"display_name":"Python 3","name":"python3"},"language_info":{"name":"python"}},"nbformat":4,"nbformat_minor":0} ================================================ FILE: examples/sites/blog.coldtype.xyz/pages/posts/truchet-experiments.ipynb ================================================ {"cells":[{"cell_type":"code","execution_count":1,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":6,"status":"ok","timestamp":1706210575100,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"1muLkAwDv2lJ","outputId":"d82df5a0-db7a-43cc-fdac-ec68120044b4"},"outputs":[{"data":{"text/plain":["{'author': 'Rob Stenson',\n"," 'location': 'Monrovia, California',\n"," 'slug': 'truchet-experiments',\n"," 'title': 'Truchet Experiments',\n"," 'date': '11/12/2022',\n"," 'description': 'Experimenting with programmatically generating and animating Truchet tiles'}"]},"execution_count":1,"metadata":{},"output_type":"execute_result"}],"source":["dict(\n"," author=\"Rob Stenson\",\n"," location=\"Monrovia, California\",\n"," slug=\"truchet-experiments\",\n"," title=\"Truchet Experiments\",\n"," date=\"11/12/2022\",\n"," description=\"Experimenting with programmatically generating and animating Truchet tiles\"\n",")"]},{"cell_type":"code","execution_count":null,"metadata":{"id":"_o3kYU9fDyL3"},"outputs":[],"source":["#hide-blog\n","%pip install -q \"coldtype[notebook] @ git+https://github.com/goodhertz/coldtype\"\n","from coldtype.notebook import *"]},{"cell_type":"markdown","metadata":{"id":"JyaIFVzlLEov"},"source":["### Prologue\n","\n","I’ve had an odd amount of time lately, both in that it’s odd I have time, and that the time I had have has been odd. I haven't been working much, because I've been waiting for my daughter to be born, except now I've been waiting for almost two weeks, which makes every day odder: everyday I wake up and realize my daughter is still rolling around in her mother's womb. The due date has come and gone, though \"due\" is a nebulous concept. Turns out many doctors — like many engineers — love to speak confidently about things they only vaguely understand. (Update: she’s been born!)\n","\n","Anyway, all that is to say, I don't know why I've started filling little voids in my day with [Truchet tile](https://en.wikipedia.org/wiki/Truchet_tiles) experiments.\n","\n","After putting together a [long talk about 3D](https://youtu.be/gV2laWd727U), I started craving flatness again, and then I found myself on Maurice Meilleur’s website, looking at his incredible [Truchet tile motion experiments](https://mauricemeilleur.net/truchet_tiles), trying to figure out how to recreate them in Coldtype.\n","\n","And then I started playing with those ideas, and then I started an interactive Google Colab notebook to show off some of the results, and then I rewrote the blog engine for this site to convert Google Colab notebooks into blog posts. So here we are. Odd time filled. Maybe these can fill an odd time in your life, too."]},{"cell_type":"markdown","metadata":{"id":"7VtSjGxOOfdj"},"source":["## Truchet Tiles\n","\n","We begin with a square."]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":89},"executionInfo":{"elapsed":14,"status":"ok","timestamp":1669246105069,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"NeOly12OOhC1","outputId":"5f0901ee-b872-4dd1-b5d1-64e832bc77fa"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/plain":["<®:P:RecordingPen(5mvs)>"]},"execution_count":3,"metadata":{},"output_type":"execute_result"}],"source":["rect = Rect(100, 100)\n","P().rect(rect).f(0)"]},{"cell_type":"markdown","metadata":{"id":"jp_ZQV1aOsbt"},"source":["And a circle."]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":89},"executionInfo":{"elapsed":11,"status":"ok","timestamp":1669246105069,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"GvJBhIFROunx","outputId":"b0ff0acf-e19e-43dd-bb42-67f1df6dd71c"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/plain":["<®:P:RecordingPen(6mvs)>"]},"execution_count":4,"metadata":{},"output_type":"execute_result"}],"source":["P().oval(rect).f(0)"]},{"cell_type":"markdown","metadata":{"id":"WmmoKaiuottZ"},"source":["Here they are as a single path, outlined:"]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":91},"executionInfo":{"elapsed":10,"status":"ok","timestamp":1669246105069,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"-DRVCdQRoDhZ","outputId":"c521e066-5be8-497e-ab28-c6b8a600e9bb"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/plain":["<®:P:RecordingPen(34mvs)>"]},"execution_count":5,"metadata":{},"output_type":"execute_result"}],"source":["(P().rect(rect)\n"," .oval(rect)\n"," .outline(2)\n"," .f(0))"]},{"cell_type":"markdown","metadata":{"id":"4fW9FmxJo5XW"},"source":["Here's the square with two of the circles, but here the circles have been offset by half the width and height of the square, in opposite directions."]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":141},"executionInfo":{"elapsed":6,"status":"ok","timestamp":1668709372546,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"2SYODfHToh_a","outputId":"0661a8ce-72d4-4712-dad3-d8304a5c0408"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/plain":["<®:P:/3...>"]},"execution_count":6,"metadata":{},"output_type":"execute_result"}],"source":["(P().rect(rect)\n"," .ups()\n"," .append(P().oval(rect).t(rect.w/2))\n"," .append(P().oval(rect).t(-rect.w/2))\n"," .outline(2)\n"," .f(0))"]},{"cell_type":"markdown","metadata":{"id":"6Qt2jLuRIwT3"},"source":["And here’re those three shapes again, except this time the circles have been cut away from the square, rather than added to it (the `append` has become a `difference`)."]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":145},"executionInfo":{"elapsed":244,"status":"ok","timestamp":1668709372784,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"nXjMlE-Ogr7S","outputId":"2da1809a-89ac-42b9-d115-289bc28e588d"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/plain":["<®:P:RecordingPen(29mvs) {frame=Rect(0.00,0.00,100.00,100.00)}>"]},"execution_count":7,"metadata":{},"output_type":"execute_result"}],"source":["tile = (P().rect(rect)\n"," .difference(P().oval(rect).t(rect.w/2))\n"," .difference(P().oval(rect).t(-rect.w/2))\n"," .f(0)\n"," .data(frame=rect)\n"," .nshow())\n","\n","# Maybe clearer what the shape is if it's outlined\n","\n","tile.copy().outline(2)"]},{"cell_type":"markdown","metadata":{"id":"BQv0YIYiJ5jl"},"source":["If you repeat that tile in the x and y dimensions, you start to see the appeal of the tiles when patterned."]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":421},"executionInfo":{"elapsed":4,"status":"ok","timestamp":1668709372785,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"2T7BvxvPpA8i","outputId":"ea021278-3b0c-4631-95fc-4a9731d06200"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"}],"source":["n = 8\n","\n","truchet0 = (tile.copy()\n"," .layer(n)\n"," .spread()\n"," .layer(n)\n"," .stack()\n"," .nshow())"]},{"cell_type":"markdown","metadata":{"id":"IZzCouIYKCLT"},"source":["That can be shorter though.\n","\n","`.layer(n)​.spread()​.layer(n)​.stack()` means copy the original path `n` times (via `layer`) and then `spread` them out horizontally. And then copy that one spread-out row `n` more times (again with `layer`), and then `stack` those new copies vertically. (Stack is the vertical version of spread).\n","\n","I started doing this pattern a lot, so I added a function that encapsulates that idiom: `.gridlayer` — a method that does those four operations as a single operation, like this:"]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":421},"executionInfo":{"elapsed":3,"status":"ok","timestamp":1668709372785,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"wk5RMJkKKBMn","outputId":"08a87001-8136-4654-a342-96d71305a5ca"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"}],"source":["truchet1 = (tile.copy()\n"," .gridlayer(n)\n"," .nshow())"]},{"cell_type":"markdown","metadata":{"id":"9jvl9YpJKBiT"},"source":["Because these are Truchet tiles, we can rotate them to create interesting patterns. In this first rotation, we'll rotate every other one (i.e. rotate \"by column\") to get a kind of sine wave look.\n","\n","This is where the `.mapvrc` function becomes very useful: we can **map** by **r**ow and **c**olumn. Which is why the `lambda` here gets three arguments: r, c, p — row, column, path."]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":239},"executionInfo":{"elapsed":175,"status":"ok","timestamp":1668709372957,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"tgSfysButCay","outputId":"78c0050b-0392-406f-9225-188fadef7c7c"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/plain":["<®:P:/8...>"]},"execution_count":10,"metadata":{},"output_type":"execute_result"}],"source":["(truchet1\n"," .copy()\n"," .scale(0.5)\n"," .mapvrc(lambda r, c, p: p.rotate(90 if c%2 else 0)))"]},{"cell_type":"markdown","metadata":{"id":"8AsTQAPSKjbs"},"source":["A special case of mapping by row & column is `mapvch` — shorthand for **m**ap-**ch**ecker. You get two args here in your callback: the first is a boolean that indicates whether or not you’re in a \"black\" square on a checkerboard.\n","\n","Here we rotate 90° if `b` is `True`, which gets us this nice offset circles pattern."]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":239},"executionInfo":{"elapsed":3,"status":"ok","timestamp":1668709372957,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"C_7Glns2tMoQ","outputId":"77f52d83-67fb-42ae-8316-5f733c1fcdbf"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/plain":["<®:P:/8...>"]},"execution_count":11,"metadata":{},"output_type":"execute_result"}],"source":["(truchet1\n"," .copy()\n"," .scale(0.5)\n"," .mapvch(lambda b, p: p.rotate(90 if b else 0)))"]},{"cell_type":"markdown","metadata":{"id":"44pt8PggJnQk"},"source":["Maybe a little easier to understand if we add a little color. Here the green tiles have been rotated 90 degrees; the blue tiles have not been rotated at all."]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":239},"executionInfo":{"elapsed":212,"status":"ok","timestamp":1668709373167,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"9IzDb7LGJPto","outputId":"bbe91811-3349-4115-de76-1c26781cd7bf"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/plain":["<®:P:/8...>"]},"execution_count":12,"metadata":{},"output_type":"execute_result"}],"source":["(truchet1\n"," .copy()\n"," .scale(0.5)\n"," .mapvch(lambda b, p: p\n"," .rotate(90 if b else 0)\n"," .f(hsl(0.6) if b else hsl(0.3))))"]},{"cell_type":"markdown","metadata":{"id":"y5_Oa91aK_zT"},"source":["Of course, it’s just as much fun to rotate things randomly. Here we accomplish that with a `random_series` (which lets us get repeatable randomization), and the ever-useful `mapv` function, which optionally gives us two args in our lambda. (If we ask for two args, the first is an index, the second is the path.)"]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":239},"executionInfo":{"elapsed":5,"status":"ok","timestamp":1668709373168,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"LNoYZ6AvqXmv","outputId":"6dcacb48-f11b-466c-e4ae-b596322f523a"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/plain":["<®:P:/8...>"]},"execution_count":13,"metadata":{},"output_type":"execute_result"}],"source":["rs = random_series(-2, 2, seed=2)\n","\n","(truchet1\n"," .copy()\n"," .scale(0.5)\n"," .mapv(lambda i, p: p.rotate(90*int(rs[i]))))"]},{"cell_type":"markdown","metadata":{"id":"yTmhTVaTxIjp"},"source":["# Variations\n","\n","And now I'd like to freestyle some patterns based on the above.\n","\n","One key difference from the above: in these patterns, we’re creating the \"tile\" on the fly and layering it into a pattern — all in a single expression.\n","\n","(The `nshow` call lets us view an _intermediate_ result, which means we can see how the tile looks before the rest of the patterning code.)"]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":501},"executionInfo":{"elapsed":477,"status":"ok","timestamp":1668709373641,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"2EO14TTlvXQO","outputId":"4a132576-31a0-498a-921e-cc4a22f3c4a0"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/plain":["<®:P:/64...>"]},"execution_count":14,"metadata":{},"output_type":"execute_result"}],"source":["# Outlining the tiles\n","\n","rs2 = random_series(-2, 2, seed=1)\n","\n","(P().rect(rect)\n"," .difference(P().oval(rect).t(rect.w/2))\n"," .difference(P().oval(rect).t(-rect.w/2))\n"," .f(0)\n"," .data(frame=rect)\n"," .nshow()\n"," .gridlayer(n)\n"," .mapv(lambda i, p: p.rotate(90*int(rs2[i])))\n"," .collapse()\n"," .mapv(lambda p: p.outline(8)))"]},{"cell_type":"markdown","metadata":{"id":"GX6DEILnMDmc"},"source":["(If you were to uncomment the xor in this one, you'd get an inverse pattern.)"]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":493},"executionInfo":{"elapsed":210,"status":"ok","timestamp":1668709374041,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"Iz1Smzunvv2E","outputId":"32851169-0179-42d2-b03c-91bf7c1d5737"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/plain":["<®:P:/8...>"]},"execution_count":15,"metadata":{},"output_type":"execute_result"}],"source":["(P().rect(rect)\n"," .difference(P().oval(rect).t(rect.w/2).outline(4))\n"," .difference(P().oval(rect).t(-rect.w/2).outline(4))\n"," #.xor(P(rect))\n"," .f(0)\n"," .nshow()\n"," .data(frame=rect)\n"," .gridlayer(n)\n"," .mapv(lambda i, p: p.rotate(90*int(rs2[i]))))"]},{"cell_type":"markdown","metadata":{"id":"ZmAVEEy9MI6K"},"source":["This one kind of looks like tennis balls?"]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":492},"executionInfo":{"elapsed":348,"status":"ok","timestamp":1668709374387,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"4NQsDs9lwiNz","outputId":"94ead3b6-8fbe-4d32-f1ff-2d8ccad43a78"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/plain":["<®:P:/8...>"]},"execution_count":16,"metadata":{},"output_type":"execute_result"}],"source":["(P().oval(rect)\n"," .difference(P().oval(rect).t(rect.w/2).outline(6))\n"," .difference(P().oval(rect).t(-rect.w/2).outline(6))\n"," .f(0)\n"," .nshow()\n"," .data(frame=rect)\n"," .gridlayer(n)\n"," .mapv(lambda i, p: p.rotate(90*int(rs2[i]))))"]},{"cell_type":"markdown","metadata":{"id":"RG7liKp5MOAg"},"source":["I'd love to see a wall that looked this:"]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":481},"executionInfo":{"elapsed":4,"status":"ok","timestamp":1668709374387,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"E-brZ6FLwyqr","outputId":"72519a51-2aea-44cb-db9d-c4500cda6873"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/plain":["<®:P:/8...>"]},"execution_count":17,"metadata":{},"output_type":"execute_result"}],"source":["(P().rect(rect)\n"," .difference(P().oval(rect).t(rect.w/2).outline(6))\n"," .difference(P().oval(rect).t(-rect.w/2).outline(6))\n"," .intersection(P(rect.inset(6)))\n"," .f(0)\n"," .nshow()\n"," .data(frame=rect)\n"," .gridlayer(n)\n"," .mapv(lambda i, p: p.rotate(90*int(rs2[i]))))"]},{"cell_type":"markdown","metadata":{"id":"eYfBED8PMVx2"},"source":["Here the initial pattern changes — cutting circles out of a circle, rather than circles from a square."]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":493},"executionInfo":{"elapsed":291,"status":"ok","timestamp":1668709374675,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"ucKpbklbxCaX","outputId":"ee1b31a4-137b-4387-e665-4845a9365720"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/plain":["<®:P:/8...>"]},"execution_count":18,"metadata":{},"output_type":"execute_result"}],"source":["(P().oval(rect)\n"," .difference(P().oval(rect).t(rect.w/2))\n"," .difference(P().oval(rect).t(-rect.w/2))\n"," .f(0)\n"," .nshow()\n"," .gridlayer(n)\n"," .mapv(lambda i, p: p.rotate(90*int(rs2[i]))))"]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":493},"executionInfo":{"elapsed":234,"status":"ok","timestamp":1668709374905,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"nu0b17VPyZaM","outputId":"d31bf401-a661-4bbb-ae76-1a58b69e1eb2"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/plain":["<®:P:/8...>"]},"execution_count":19,"metadata":{},"output_type":"execute_result"}],"source":["(P().rect(rect)\n"," .difference(P().oval(rect).t(rect.w/2).outline(2))\n"," .difference(P().oval(rect).t(-rect.w/2).outline(2))\n"," .f(0)\n"," .nshow()\n"," .data(frame=rect)\n"," .gridlayer(n)\n"," .mapv(lambda i, p: p.rotate(90*int(rs2[i])))\n"," .mapvch(lambda b, p: p.xor(P().rect(p.ambit(th=1, tv=1).inset(0))) if b else p)\n"," #.f(hsl(0.9, 0.8, 0.6))\n"," )"]},{"cell_type":"markdown","metadata":{"id":"zfFykcmQMjfr"},"source":["# Hexagons\n","\n","Here’s a variation on the square tiles above, this time using a hexagon tile with a pattern I (once again) cribbed [from Maurice Meilleur](https://mauricemeilleur.net/truchet_tiles).\n","\n","An open question: does this pattern have something to do with Aphex Twin?\n","\n","(I’ll admit: because I'm very bad at math, this one took me a very long time to figure out, and also the code is much less flowing.)"]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":535},"executionInfo":{"elapsed":297,"status":"ok","timestamp":1668709375200,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"DnW0_mV0OmGP","outputId":"90848ac1-a78d-4a12-9db3-56da25646327"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"}],"source":["from coldtype.fx.shapes import polygon\n","\n","r = Rect(120, 120)\n","\n","hex = P().ch(polygon(6, r))\n","hexr = hex.ambit(th=1, tv=1)\n","p1y = hex._val.value[0][-1][-1][-1]\n","\n","rr = r.w/4\n","\n","hex_pattern = (hex.copy()\n"," .intersection(P()\n"," .append(P().oval(r.inset(rr)).t(0, rr*2))\n"," .append(P().rect(hexr.take(rr*2, \"S\"))\n"," .difference(P().oval(r.inset(rr)).t(0, -rr*2))))\n"," .f(0)\n"," .data(frame=hexr)\n"," .nshow()\n"," .gridlayer(10, lead=-hexr.h-p1y)\n"," .mapvrc(lambda r, c, p: p.t(hexr.w/2 if not r%2 else 0, 0))\n"," .mapv(lambda i, p: p.rotate(120*int(rs2[i]), th=0, tv=0))\n"," .fssw(0, 0, 2)\n"," .nshow())\n"]},{"cell_type":"markdown","metadata":{"id":"MsyZY-onNACr"},"source":["Here’s that same pattern altered slightly and sent through a rasterization pass with `phototype`, which we’re using here to soften the edges of all the boolean path operations."]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":379},"executionInfo":{"elapsed":568,"status":"ok","timestamp":1668709375767,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"tTTfT92NqHjH","outputId":"062e10b2-8cba-494a-f908-c5323d178be1"},"outputs":[{"data":{"text/html":[""],"text/plain":[""]},"metadata":{},"output_type":"display_data"},{"data":{"text/plain":["<®:P:RecordingPen(5mvs)>"]},"execution_count":21,"metadata":{},"output_type":"execute_result"}],"source":["from coldtype.fx.skia import phototype\n","\n","(hex_pattern\n"," .copy()\n"," .pen()\n"," .align(Rect(1000, 1000))\n"," .unframe()\n"," .intersection(P(Rect(1000, 1000).take(800, \"CX\").square()))\n"," .outline(4)\n"," .fssw(1, 0, 0)\n"," .zero()\n"," .ch(phototype(Rect(800, 800).inset(60), blur=3, cutw=20, fill=0)))"]},{"cell_type":"markdown","metadata":{"id":"hsPg8SkPNrYr"},"source":["# Animation"]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":321,"referenced_widgets":["3fd5f0ced32348f181e22bc3efee9c2a","fdef570afc144d5e84ca92dfc6b588ef","73c9ad6897cb427f9ef5e6ba0f1541ce","c8250b3204aa4e3ab8834240590c588b","062c6adcfdca4b63a24cdb6a08b82978","0ebae87610d64e51a95cc461a9017360","d2caef1c5c214d8fb25b370bf3c6dfeb","8047f7d4afee447184239945629abcb2","13eeb928c6ad4f2f91e0e72581196ab7","9145c42c85cf4132b4ecf7a36f3630e4","16f3d0d32040430fa33aaaaa26210423"]},"executionInfo":{"elapsed":4324,"status":"ok","timestamp":1668709580564,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"oqT8gwbLwbHk","outputId":"1e047a75-9769-417a-ea0f-d2c53d4cd804"},"outputs":[{"data":{"text/html":["\n"," \n"," "],"text/plain":[""]},"metadata":{},"output_type":"display_data"}],"source":["@animation((600, 600), bg=1, tl=30, render_show=True)\n","def tennis_balls(f):\n"," tb = f.a.r.take(100, \"SW\")\n"," return (P().oval(tb)\n"," .difference(P().oval(tb).t(tb.w/2).outline(6))\n"," .difference(P().oval(tb).t(-tb.w/2).outline(6))\n"," .f(0)\n"," .data(frame=tb)\n"," .gridlayer(6)\n"," .mapv(lambda i, p: p.rotate(90*int(rs2[i])))\n"," .mapv(lambda i, p: p.rotate(f.e(\"eeio\", 0, r=(0, 180)))))"]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":271,"referenced_widgets":["cd64aeedd7b1451e9df97136a3826719","d4c56c7f2c074beebd177dfb560ea013","9416f6985cc9429483ab4c351e12fa0f","ae29ce027cd4460c95c4b9af36e2b386","01dd4a2d8f274198af1d9b6d88836bde","bd00098e63554691b82d323b851e0a51","2100c17b674141aaab7f30692c59ae21","e5cde23f6e47408f84ce041c1eca4c0a","1ab616a121a14f1d8bf00f508f597a6a","ebf1182e99e24bdcb55992ca431a0054","7f5eb12ff6fb4a239a631bd83155a35b"]},"executionInfo":{"elapsed":17608,"status":"ok","timestamp":1668709473978,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"FXLPhqFxNtOa","outputId":"e69a43b3-5450-43ea-eb06-61f358af03bd"},"outputs":[{"data":{"text/html":["\n"," \n"," "],"text/plain":[""]},"metadata":{},"output_type":"display_data"}],"source":["tr = Rect(100)\n","tn = 5\n","\n","at = AsciiTimeline(7, 30, \"\"\"\n","<\n","[0 ] [1 ]\n"," [1 ] [0 ]\n"," [2 ] [2 ]\n"," [3 ] [3 ]\n"," [4 ] [4 ]\n"," [5 ] [5 ] <\n"," [6 ] [6 ]\n","\"\"\")\n","\n","@animation((500, 500), tl=at, bg=1, render_show=True)\n","def offset_turns(f):\n"," return (P(tr)\n"," .difference(P()\n"," .append(P().oval(tr).t(tr.w/2))\n"," .append(P().oval(tr).t(-tr.w/2)))\n"," .f(0)\n"," .data(frame=tr)\n"," .gridlayer(tn)\n"," .mapv(lambda i, p: p.rotate(at.ki(f\"{i%6}\").ec(\"eeio\", (0, 90)))))"]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":271,"referenced_widgets":["fdb2e6bcf5684aa18e858a1ae89d0e93","618d8b346950454ca0543d6b16027749","0edf883083694bbca7d8fe1e2a159d8d","4d9e3562cdc94126b7fc27c1592dff94","1661eba632684a14bccce00e96d8ae82","28d93f13345f4dbc9baef75b13e8b8bc","ce5ab8240ec049f18c83831f8cf4b368","fa8a919d90c74fa8aa9e9f6bb8cc6d5d","a065ba9b3e654b4c80e69d3d55bfb190","86d1f226b7244854b6cebe5e4eb2e751","27e54603485b4200bf4cfcda2f855a93"]},"executionInfo":{"elapsed":15114,"status":"ok","timestamp":1668709494688,"user":{"displayName":"Rob Stenson","userId":"15987375555675088907"},"user_tz":480},"id":"QV1AvUVgP22J","outputId":"ae6b9035-8a9e-42e5-bb81-b46a3c8ad8c4"},"outputs":[{"data":{"text/html":["\n"," \n"," "],"text/plain":[""]},"metadata":{},"output_type":"display_data"}],"source":["@animation((500, 500), tl=at, bg=1, render_show=True)\n","def offset_turns_colorful(f):\n"," return (P(tr)\n"," .difference(P()\n"," .append(P().oval(tr).t(tr.w/2))\n"," .append(P().oval(tr).t(-tr.w/2)))\n"," .f(0)\n"," .data(frame=tr)\n"," .gridlayer(tn)\n"," .mapv(lambda i, p: p\n"," .rotate(at.ki(f\"{i%6}\").ec(\"eeio\", (0, 90)))\n"," .f(hsl(i%6/6))))"]}],"metadata":{"colab":{"authorship_tag":"ABX9TyPvZHh5L7/1eANuk/20JoaR","provenance":[]},"kernelspec":{"display_name":"Python 3","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.7"},"widgets":{"application/vnd.jupyter.widget-state+json":{"01dd4a2d8f274198af1d9b6d88836bde":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":"hidden","width":null}},"062c6adcfdca4b63a24cdb6a08b82978":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":"hidden","width":null}},"0ebae87610d64e51a95cc461a9017360":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"0edf883083694bbca7d8fe1e2a159d8d":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"ProgressView","bar_style":"","description":"","description_tooltip":null,"layout":"IPY_MODEL_fa8a919d90c74fa8aa9e9f6bb8cc6d5d","max":301,"min":0,"orientation":"horizontal","style":"IPY_MODEL_a065ba9b3e654b4c80e69d3d55bfb190","value":301}},"13eeb928c6ad4f2f91e0e72581196ab7":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"1661eba632684a14bccce00e96d8ae82":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":"hidden","width":null}},"16f3d0d32040430fa33aaaaa26210423":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"DescriptionStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"DescriptionStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","description_width":""}},"1ab616a121a14f1d8bf00f508f597a6a":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"2100c17b674141aaab7f30692c59ae21":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"DescriptionStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"DescriptionStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","description_width":""}},"27e54603485b4200bf4cfcda2f855a93":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"DescriptionStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"DescriptionStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","description_width":""}},"28d93f13345f4dbc9baef75b13e8b8bc":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"3fd5f0ced32348f181e22bc3efee9c2a":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_fdef570afc144d5e84ca92dfc6b588ef","IPY_MODEL_73c9ad6897cb427f9ef5e6ba0f1541ce","IPY_MODEL_c8250b3204aa4e3ab8834240590c588b"],"layout":"IPY_MODEL_062c6adcfdca4b63a24cdb6a08b82978"}},"4d9e3562cdc94126b7fc27c1592dff94":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"HTMLView","description":"","description_tooltip":null,"layout":"IPY_MODEL_86d1f226b7244854b6cebe5e4eb2e751","placeholder":"​","style":"IPY_MODEL_27e54603485b4200bf4cfcda2f855a93","value":" 299/301 [00:11<00:00, 27.18it/s]"}},"618d8b346950454ca0543d6b16027749":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"HTMLView","description":"","description_tooltip":null,"layout":"IPY_MODEL_28d93f13345f4dbc9baef75b13e8b8bc","placeholder":"​","style":"IPY_MODEL_ce5ab8240ec049f18c83831f8cf4b368","value":" 99%"}},"73c9ad6897cb427f9ef5e6ba0f1541ce":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"ProgressView","bar_style":"","description":"","description_tooltip":null,"layout":"IPY_MODEL_8047f7d4afee447184239945629abcb2","max":30,"min":0,"orientation":"horizontal","style":"IPY_MODEL_13eeb928c6ad4f2f91e0e72581196ab7","value":30}},"7f5eb12ff6fb4a239a631bd83155a35b":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"DescriptionStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"DescriptionStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","description_width":""}},"8047f7d4afee447184239945629abcb2":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"86d1f226b7244854b6cebe5e4eb2e751":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"9145c42c85cf4132b4ecf7a36f3630e4":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"9416f6985cc9429483ab4c351e12fa0f":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"ProgressView","bar_style":"","description":"","description_tooltip":null,"layout":"IPY_MODEL_e5cde23f6e47408f84ce041c1eca4c0a","max":301,"min":0,"orientation":"horizontal","style":"IPY_MODEL_1ab616a121a14f1d8bf00f508f597a6a","value":301}},"a065ba9b3e654b4c80e69d3d55bfb190":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"ae29ce027cd4460c95c4b9af36e2b386":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"HTMLView","description":"","description_tooltip":null,"layout":"IPY_MODEL_ebf1182e99e24bdcb55992ca431a0054","placeholder":"​","style":"IPY_MODEL_7f5eb12ff6fb4a239a631bd83155a35b","value":" 299/301 [00:13<00:00, 18.05it/s]"}},"bd00098e63554691b82d323b851e0a51":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"c8250b3204aa4e3ab8834240590c588b":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"HTMLView","description":"","description_tooltip":null,"layout":"IPY_MODEL_9145c42c85cf4132b4ecf7a36f3630e4","placeholder":"​","style":"IPY_MODEL_16f3d0d32040430fa33aaaaa26210423","value":" 29/30 [00:02<00:00, 13.17it/s]"}},"cd64aeedd7b1451e9df97136a3826719":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_d4c56c7f2c074beebd177dfb560ea013","IPY_MODEL_9416f6985cc9429483ab4c351e12fa0f","IPY_MODEL_ae29ce027cd4460c95c4b9af36e2b386"],"layout":"IPY_MODEL_01dd4a2d8f274198af1d9b6d88836bde"}},"ce5ab8240ec049f18c83831f8cf4b368":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"DescriptionStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"DescriptionStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","description_width":""}},"d2caef1c5c214d8fb25b370bf3c6dfeb":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"DescriptionStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"DescriptionStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","description_width":""}},"d4c56c7f2c074beebd177dfb560ea013":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"HTMLView","description":"","description_tooltip":null,"layout":"IPY_MODEL_bd00098e63554691b82d323b851e0a51","placeholder":"​","style":"IPY_MODEL_2100c17b674141aaab7f30692c59ae21","value":" 99%"}},"e5cde23f6e47408f84ce041c1eca4c0a":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"ebf1182e99e24bdcb55992ca431a0054":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"fa8a919d90c74fa8aa9e9f6bb8cc6d5d":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"fdb2e6bcf5684aa18e858a1ae89d0e93":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_618d8b346950454ca0543d6b16027749","IPY_MODEL_0edf883083694bbca7d8fe1e2a159d8d","IPY_MODEL_4d9e3562cdc94126b7fc27c1592dff94"],"layout":"IPY_MODEL_1661eba632684a14bccce00e96d8ae82"}},"fdef570afc144d5e84ca92dfc6b588ef":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"HTMLView","description":"","description_tooltip":null,"layout":"IPY_MODEL_0ebae87610d64e51a95cc461a9017360","placeholder":"​","style":"IPY_MODEL_d2caef1c5c214d8fb25b370bf3c6dfeb","value":" 97%"}}}}},"nbformat":4,"nbformat_minor":0} ================================================ FILE: examples/sites/blog.coldtype.xyz/templates/_footer.j2 ================================================ ================================================ FILE: examples/sites/blog.coldtype.xyz/templates/_header.j2 ================================================ {% if url == "/" %}

The Coldtype Blog

{% else %}

The Coldtype Blog

{% endif %} ================================================ FILE: examples/sites/blog.coldtype.xyz/templates/_post.j2 ================================================

{{ page.title }}

{{ page.content|safe }}
================================================ FILE: examples/sites/blog.coldtype.xyz/templates/index.j2 ================================================ ================================================ FILE: examples/sites/coldtype.goodhertz.com/assets/style.css ================================================ @import "examples/sites/blog.coldtype.xyz/assets/style.css"; .wrapper { max-width: 950px; margin: auto auto; padding: 20px; } .index .wrapper { max-width: 500px; } .page { display: flex; } .page aside { width: 30%; font-size: 85%; } .page .post { width: 70%; } @media (max-width: 700px) { .page aside { display: none; } .page .post { width: 100%; } } header h1 { text-align: center; } .post h1 { margin-top: 0px; } .colab-callout { margin-bottom: 0px; } .menu { padding-right: 10px; } .menu h3 { --mono-font: fvs(wght=0.75); text-transform: uppercase; margin-top: 20px; margin-bottom: 10px; } .menu a { --mono-font: fvs(wght=0.65); padding: 2px 2px 2px 20px; } .menu a:hover { background-color: hsl(330, 80%, 95%); } .menu .current { color: #555; } ================================================ FILE: examples/sites/coldtype.goodhertz.com/coldtype.goodhertz.com.py ================================================ from coldtype import * from coldtype.web.site import * from functools import partial import inspect, markdown from time import time from textwrap import dedent from coldtype.runon import Runon from collections import namedtuple from pygments import highlight from pygments.lexers import PythonLexer from pygments.formatters import HtmlFormatter from lxml.html import fragment_fromstring, tostring classes = [ P, Style, Rect, Scaffold, Runon ] decorators = [ renderable, animation, ] functions = [ StSt, Glyphwise ] root = Path("./coldtype").resolve() output = { "decorators": [], "classes": [], "functions": [], } doc = namedtuple("doc", ["itself", "path", "docstring", "signature", "methods"]) def classes_functions(s:site): for k, v in output.items(): for x in globals()[k]: docstring = inspect.getdoc(x) if docstring: docstring_md = markdown.markdown(docstring, extensions=["smarty", "mdx_linkify", "fenced_code", "codehilite"], extension_configs={"codehilite":{"css_class":"highlight"}}) path = Path(inspect.getfile(x)).relative_to(root) sig = None src = None methods = [] if inspect.isclass(x): src = inspect.getsource(getattr(x, "__init__")) src = dedent(f"class {x.__name__}:\n" + src.split("):")[0] + "):") _methods = inspect.getmembers(x) for m in _methods: try: _path = Path(inspect.getfile(m[1])).relative_to(root) except (TypeError, ValueError): _path = None if _path == path and m[1].__name__ not in ["__init__", "__call__", "__repr__", "__eq__"]: ds = inspect.getdoc(m[1]) if ds: ds_md = markdown.markdown(ds, extensions=["smarty", "mdx_linkify", "fenced_code", "codehilite"], extension_configs={"codehilite":{"css_class":"highlight"}}) _src = dedent(inspect.getsource(m[1]).split("->")[0]) _highlit = fragment_fromstring(highlight(_src, PythonLexer(), HtmlFormatter(linenos=False))) _sig = tostring(_highlit, pretty_print=True, encoding="utf-8").decode("utf-8") methods.append(doc(m[1], path, ds_md, _sig, None)) methods = sorted([*set(methods)], key=lambda m: m.itself.__name__) else: src = dedent(inspect.getsource(x).split("->")[0]) if src: highlit = fragment_fromstring(highlight(src, PythonLexer(), HtmlFormatter(linenos=False))) sig = tostring(highlit, pretty_print=True, encoding="utf-8").decode("utf-8") output[k].append(doc(x.__name__, path, docstring_md, sig, methods)) #if inspect.isclass(x): # print(path, k, inspect.isclass(x)) page = Page(None, None, f"classes_functions.html", "Classes & Functions", "_docs", None, None, None, None) s.render_page(page, dict(docs=output)) return page hierarchy = { "mains": ["introduction.html", "about.html", "overview.html", "install.html", "classes_functions.html"], "tutorials": ["tutorials/shapes.html", "tutorials/geometry.html", "tutorials/text.html", "tutorials/animation.html", "tutorials/drawbot.html", "tutorials/blender.html"], "cheatsheets": ["cheatsheets/viewer.html", "cheatsheets/easing.html", "cheatsheets/rectangles.html", "cheatsheets/oneletter.html", "cheatsheets/text.html"], } def section(section, site): return sorted([p for p in site.pages if p.slug in hierarchy[section]], key=lambda p: hierarchy[section].index(p.slug)) info = dict( title="Coldtype Tutorials", description="These are Coldtype tutorials", navigation={ #"Home": "/", #"About": "/about" "coldtype.xyz": "https://coldtype.xyz/", "blog": "https://blog.coldtype.xyz/", "github": "https://github.com/coldtype", "youtube": "https://www.youtube.com/channel/UCIRaiGAVFaM-pSErJG1UZFA", "q&a forum": "https://github.com/goodhertz/coldtype/discussions", }) @site(ººsiblingºº(".") , port=8008 , sources=dict( mains=partial(section, "mains"), tutorials=partial(section, "tutorials"), cheatsheets=partial(section, "cheatsheets")) , generators=dict( classes_functions=classes_functions) , info=info , template=lambda _: "_page" , slugs="nested,html" , fonts={ "text-font": dict(regular="MDSystem-VF"), "mono-font": dict(regular="MDIO-VF")}) def website(_): website.build() def release(_): website.upload("coldtype.goodhertz.com", "us-east-1", None) ================================================ FILE: examples/sites/coldtype.goodhertz.com/pages/about.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'author': 'Rob Stenson', 'title': 'About', 'date': '11/29/2022'}" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dict(\n", " author=\"Rob Stenson\",\n", " title=\"About\",\n", " date=\"11/29/2022\"\n", ")" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "#hide-publish\n", "%pip install -q \"coldtype[notebook]\"\n", "#!pip install -q \"coldtype[notebook] @ git+https://github.com/goodhertz/coldtype\"\n", "from coldtype.notebook import *" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## What is Coldtype?\n", "\n", "*Coldtype is a cross-platform library to help you precisely & programmatically do display typography with Python.*\n", "\n", "Here’s a brief example of some text in a box." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((1200, 350), bg=1)\n", "def render(r):\n", " return (P(\n", " P(r.inset(10)).outline(5).f(0),\n", " StSt(\"COLDTYPE\", Font.ColdtypeObviously(), 250,\n", " wdth=1, tu=-170, r=1, rotate=15, kp={\"P/E\":-100})\n", " .align(r)\n", " .fssw(0, 1, 20, 1)\n", " .translate(0, 4)))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Some oddities to note if you’re familiar with other graphics programming environments:\n", "\n", "* There is no \"canvas\"; all graphics are *structured data* `return`-ed from a function to the renderer, which does all the actual drawing-to-a-virtual-canvas.\n", "\n", "* There is an idiomatic emphasis on *method-chaining*, which (I feel) is an underappreciated way to do graphics programming, since the resulting code is easily editable and experimentable. Of course, some (like the creator of Python) have called this style of programming \"un-Pythonic.\" Take from that what you will!\n", "\n", "* As might already be clear from the first two points, coldtype is not meant to be a good introduction to programming. The point here is to be a toolkit that can help you create professional graphics, particularly *complex animations* that blend multiple data sources.\n", "\n", "\n", "## Why is Coldtype?\n", "\n", "There are lots of ways to set type with code. Most ways — HTML/CSS/JS, Processing, etc. — are great for 90% of what most people want to do with Latin-based writing systems. Then the road runs out and you can’t do anything else.\n", "\n", "Coldtype is an offroad vehicle that lets you keep driving where there are no roads. Like many vehicles built for specialized use, it is not particularly user-friendly. It has no doors (please climb in through the window), and the steering wheel is not very intuitive, also it’s got a manual transmission, and you should probably know how to code (or be willing to learn) if you’re going to drive it alone out into the desert. (I apologize for how automotive this metaphor is getting. Probably should’ve gone with some metaphor about people making custom synthesizers in the 70s.)\n", "\n", "## What about DrawBot?\n", "\n", "If you’ve heard of [DrawBot](https://drawbot.com/) — another offroad vehicle — you may be wondering how Coldtype is different. The answer is that Coldtype provides a very different programming idiom, one based around creating and modifying structured data, rather than — as is common in most creative coding platforms (including DrawBot and Processing) — an idiom based around a metaphorical canvas that you render to directly.\n", "\n", "I should point out that DrawBot is fantastic and that Coldtype would not exist without DrawBot, mostly because using DrawBot was my first time driving in the typographical offroad. That said, Coldtype exists somewhat as a response to things I found awkward when programming long-form animations with DrawBot. (Also, you can use [DrawBot as a library from within Coldtype](/tutorials/drawbot.html).)\n", "\n", "## Why not HTML/CSS/JavaScript?\n", "\n", "I think since I started doing animations with Python a couple years ago (using DrawBot), typographic tools in JS have gotten a lot better, but I always found it awkward to program animations in JS, since I never found a good way to run a headless browser when I needed to rasterize frames for an animation. That said, the programming style of Coldtype is very influenced by JS programming patterns (like method-chaining and liberal use of anonymous functions), so if you're familiar with JS, you might feel at home writing a Coldtype program.\n", "\n", "## What about Adobe products?\n", "\n", "I’ve learned over the last few years to distrust any *Type Tool* in an Adobe product (or anywhere else). Yes, those can be very good — like HTML+CSS — for doing simple Latin-based typography for static designs. But then, all of a sudden, they are very bad. You can think of Adobe products as a train that you get on and you can fall asleep in a nice seat and the train will get you where you want to go, except when you wake up and realize you wanted to go somewhere the train doesn't go and you think *i guess i’ll walk there* (Walking in this metaphor is when you right click and hit *Convert to Outlines*.)\n", "\n", "Walking can be a lot of fun, and you get to see a lot. Drawing is a lot like walking. Fabulous exercise; great learning experience. But sometimes you want to get there faster or you want to go farther.\n", "\n", "## What can coldtype do?\n", "\n", "* [Vulfpeck, “LAX”](https://www.youtube.com/watch?v=NzxW8nxgENA)\n", "\n", "* [\"Buggin’ Out (Phife Dawg’s Verse)](https://vimeo.com/377148622)\n", "\n", "* [A 3D type specimen](https://vimeo.com/354292807)\n", "\n", "* [Goodhertz plugins](https://goodhertz.com)\n", "\n", "* Anything recent on [robstenson.com](https://robstenson.com)\n", "\n", "## How does coldtype rasterize graphics?\n", "\n", "Coldtype is written in a modular fashion, to allow rasterization/vectorization using a number of different backends. For most of its life before October 2020, I used Coldtype as a frontend to the DrawBot rasterizer (itself a frontend to the CoreGraphics rasterization engine), as well as a frontend for a custom JSON-serializer (used for Goodhertz plugins). You can still use Coldtype with DrawBot as the rasterizer (or with [DrawBot as a direct canvas](tutorials/drawbot.html)), but as of now, Coldtype by default rasterizes using the [skia-python](https://kyamagu.github.io/skia-python) package, which is cross-platform, quite fast, and has great support for image manipulation, via GL shaders.\n", "\n", "You can also use Coldtype to draw graphics directly with the skia-python package, as demonstrated in the `test/test_skia_direct.py` file in this repository.\n", "\n", "There is also support for (in varying degrees of quality): SVG, Cairo, Blender, and AxiDraw (a robotic drawing machine). (TODO add tutorial links for all of these, well except for Cairo, skia-python is just better than Cairo.)\n", "\n", "## Why “coldtype”?\n", "\n", "Coldtype refers to the short-lived era of early [semi-digital typesetting](https://en.wikipedia.org/wiki/Phototypesetting) (extending roughly from the late 1940s to the widespread adoption of personal computing in the early 1990s), during which time computers were used to control various analog photographic processes for setting type, technologies now known, usually, as “phototype,” but sometimes also known as “coldtype,” to distinguish it from hot-metal type, which was the previous standard.\n", "\n", "Phototype/coldtype was a hybrid moment in typographic history, and a fascinating one — 500 years of metal-type-based assumptions were upended all at once, as letters now did not need to live on a rectangular metal body, meaning they could get really close together, and designers could begin to think of type as a 2D material that could be layered and manipulated in new and exciting ways. To me, some of the spirit of that time has been lost with mainstream digital typesetting tools, which in many ways preserve more of the spirit of metal type than the spirit of phototype. That is, today's tools make it very easy to do many things, like set a great big column of text, but those same tools make it very difficult to do many other cool things, like pop a stylistic set on and off while varying a WDTH axis and re-ordering glyphs from left-to-right, so they overlap properly. This library is a way to make some of those difficult things easy; consequently, many of the easy things become difficult.\n", "\n", "## Is Coldtype capitalized?\n", "\n", "I can’t decide, as you may be able to tell from this documentation’s inconsistent capitalization scheme.\n", "\n", "## Who works on this?\n", "\n", "This library is mostly the work of me, [Rob Stenson](https://robstenson.com), but I want to acknowledge the work of some people and projects who’ve helped bring this project to life:\n", "\n", "* [Goodhertz](https://goodhertz.com) has supported the open-sourcing of this library, which was originally written to set text in audio plugin interfaces.\n", "\n", "* Coldtype Obviously is a open-source subset of the commercially-available font [Obviously](https://ohnotype.co/fonts/obviously) by OHno Type Co; s/o to James Edmondson for donating those 8 characters to this project.\n", "\n", "* Mutator Sans included for testing was written by Erik van Blokland, Copyright (c) 2017\n", "\n", "* Recursive Mono Casual Italic is an [open-source typeface](https://github.com/arrowtype/recursive) by [Arrow Type](https://www.arrowtype.com)\n", "\n", "* Coldtype also relies heavily on the incredible library [fontTools](https://github.com/fonttools/fonttools)" ] } ], "metadata": { "kernelspec": { "display_name": "venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.11" }, "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "10cf79b6252b6bfa5f219a04587890ec267e7f2fde6b173960de4ad2915a3b2e" } } }, "nbformat": 4, "nbformat_minor": 2 } ================================================ FILE: examples/sites/coldtype.goodhertz.com/pages/cheatsheets/easing.md ================================================ --- title: "Easing Cheatsheet" --- In the context of easing, the first letter of an easing mnemonic refers to the type of curve, listed from gentlest to steepest: * `q` == "quadratic" * `s` == "sine" * `c` == "cubic" * `e` == "exponential" The ending of a mnemonic refers to the entry/exit of the curve: * `eio` == "ease-in-out" * `ei` == "ease-in" * `eo` == "ease-out" Put together you get can mnemonics like these: * `eeio` == "exponential-ease-in-out" * `ceo` == "cubic-ease-out" * `sei` == "sine-ease-in" etc. Given a frame object ``f`` in an ``@animation`` renderable: ```python from coldtype import * @animation(timeline=60) # duration of 60 frames def easing_example(f): square = P(f.a.r.inset(300)).f(0) return square.rotate(f.e("eeio", 1, rng=(-10, 10))) ``` In the line ``f.e("eeio", 1, rng=(-10, 10))``, the ``1`` refers to the number of loops, and the ``rng=`` sets a range of values that will be traversed by the easing. By default, this value is rng=(0, 1), so the value would cycle back and forth between 0 and 1 **one-time** (loops=1) over the course of the animation's duration. If you set loops to 0, the value would traverse from 0 to 1 in only one direction and not return to 0 (meaning the value would "pop" back to 0 when the animation itself loops.) ================================================ FILE: examples/sites/coldtype.goodhertz.com/pages/cheatsheets/oneletter.md ================================================ --- title: "One-Letters Cheatsheet" --- Here are some common one-letter abbreviations you'll find throughout idiomatic Coldtype code. * ``e`` usually refers to "easing", as in the ``f.e`` function (frame.easing) * ``f`` usually refers, when used as a variable, to the ``Frame`` object that is passed to an ``@animation`` renderable. * ``.f`` refers, when used as a function, to *fill*, as in the fill color of a path * ``.s`` refers, when used as a function, to *stroke*, and ``.sw`` refers to *stroke width* * ``.fssw`` refers to fill-stroke-strokewidth as a single concept. This is something I use all the time in my own work, as I almost never set a stroke without also setting the fill (usually to -1 to be transparent). * ``r`` usually refers, when used as a variable, to the ``Rect`` object that is passed to a ``@renderable`` renderable. * ``f.a.r`` is a common shorthand for frame.animation.rect, i.e. the rectangle frame of an ``@animation`` renderable. ================================================ FILE: examples/sites/coldtype.goodhertz.com/pages/cheatsheets/rectangles.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dict(\n", " author=\"Rob Stenson\",\n", " date=\"1/6/2023\",\n", " title=\"Rectangles Cheatsheet\",\n", ")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "A quick reference for `Rect`(angles)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from coldtype import Rect\n", "\n", "# N.B. Coldtype has point (0,0) at bottom-left\n", "\n", "# three equivalent ways to declare a rect\n", "\n", "a = Rect(0, 0, 1080, 1080) # x, y, w, h\n", "b = Rect(1080, 1080) # just 2 args will be w & h\n", "c = Rect([0, 0, 1080, 1080]) # can also be 1-arg (a list)\n", "\n", "# simple modifications\n", "\n", "r = Rect(1080, 1080)\n", "a = r.inset(100, 100) # 100-px padding on all sides\n", "b = r.inset(200, 50) # 200-px inset on left side and right side, 50-px inset on the top and bottom\n", "c = r.offset(10, 20) # 10-px translation on x-axis (i.e. to-the-right), 20-px translation on y-axis (i.e. up)\n", "\n", "# getting values from a rect\n", "\n", "r = Rect(1080, 1080)\n", "r.x # x coordinate of bottom-left\n", "r.y # y coordinate of bottom-left\n", "r.w # width\n", "r.h # height\n", "# -or-\n", "r[0] # x\n", "r[1] # y\n", "r[2] # w\n", "r[3] # h\n", "x, y, w, h = r\n", "\n", "# can be splat'd\n", "def use_rect(x, y, w, h):\n", " return x + w, y + h\n", "use_rect(*r)\n", "\n", "# compass points\n", "\n", "r.pc # point-center\n", "r.pn # point-north\n", "r.ps # point-south\n", "r.pe, r.pw # point-east, point-west\n", "r.pne, r.pse, r.psw, r.pnw # northeast, southeast, southwest, northwest\n", "# all of these values yield a Point object, which has x/y props & behaves like a list\n", "r.pc.x # x coordinate of the center of the rect\n", "r.pc.y # y coordinate of the center of the rect\n", "r.pc[0] # x\n", "r.pc[1] # y\n", "\n", "# quick columns\n", "\n", "r = Rect(0, 0, 1080, 1080)\n", "a, b, c = r.subdivide(3, \"mnx\") # 'a' would be the first column, 'c' the last (left-to-right)\n", "a, b, c, d = r.subdivide(4, \"mxx\") # 'a' would be the first column, 'd' the last (right-to-right)\n", "columns = r.subdivide(8, \"W\") # columns holds a list of 8 columns arrayed west-to-east (left-to-right) (b/c of the W argument, equivalent to \"mnx\")\n", "\n", "# quick rows\n", "\n", "a, b, c, d = r.subdivide(4, \"mxy\") # 'a' would be the first row, 'd' the last (top-to-bottom)\n", "a, b, c, d = r.subdivide(4, \"mxy\") # 'a' would be the first row, 'd' the last (top-to-bottom)\n", "rows = r.subdivide(8, \"N\") # rows holds a list of 8 rows arrayed north-to-south (top-to-bottom) (b/c of the \"N\" argument, equivalent to \"mxy\")\n", "\n", "# quick slicing and dicing\n", "\n", "r = Rect(1080, 1080)\n", "r.take(100, \"W\") # 100px-wide rect sliced off the western half of the original rect, i.e. Rect(0, 0, 100, 1080)\n", "\n", "# \"edge\" shorthand\n", "\n", "\"mnx\" == \"W\" == \"⊢\" # aka minimum-x aka the left-hand edge of a rectangle, aka the western edge\n", "\"mny\" == \"S\" == \"⊥\" # aka minimum-y aka the bottom edge of a rectangle, aka the southern edge\n", "\"mxx\" == \"E\" == \"⊣\" # aka maximum-x aka the right-hand edge of a rectangle, aka the eastern edge\n", "\"mxy\" == \"N\" == \"⊤\" # aka maximum-y aka the top edge of a rectangle, aka the northern edge\n", "\"mdx\" == \"CX\" == \"⌶\" # aka middle-x aka the center vertical \"edge\", or line of a rectangle (a line going from the top to the bottom right down the middle)\n", "\"mdy\" == \"CY\" == \"H\" # aka middle-y aka the center horizontal \"edge\" of a rectangle (a line going from the left to the right right through the middle (separating the bottom half from the top half))" ] } ], "metadata": { "kernelspec": { "display_name": "venv", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.10.5 (v3.10.5:f377153967, Jun 6 2022, 12:36:10) [Clang 13.0.0 (clang-1300.0.29.30)]" }, "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "10cf79b6252b6bfa5f219a04587890ec267e7f2fde6b173960de4ad2915a3b2e" } } }, "nbformat": 4, "nbformat_minor": 2 } ================================================ FILE: examples/sites/coldtype.goodhertz.com/pages/cheatsheets/text.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dict(\n", " author=\"Rob Stenson\",\n", " title=\"Text Cheatsheet\",\n", " date=\"1/6/2023\"\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from coldtype import *\n", "\n", "# an exact path to a font file\n", "fnt = Font.Cacheable(\"/System/Library/Fonts/SFNS.ttf\")\n", "\n", "# listing fonts that match a pattern\n", "\n", "fnt = Font.List(\"Times\") # returns exact paths that can be passed to Font.Cacheable\n", "\n", "# finding and loading the first font matching a pattern\n", "\n", "fnt = Font.Find(\"Times\")\n", "\n", "# The StSt — the quickest way to get from plaintext to a vector/path representation of that text in a given font\n", "\n", "StSt(\"Text\", fnt, 100) # simplest StSt invocation\n", "\n", "# Building and aligning a StSt\n", "\n", "r = Rect(1080, 1080)\n", "(StSt(\"Text\", fnt, 100)\n", " .align(r)) # center aligns\n", "\n", "(StSt(\"Text\", fnt, 100)\n", " .align(r, \"mnx\", \"mny\")) # align to bottom-left\n", "\n", "(StSt(\"Text\", fnt, 100)\n", " .align(r, \"⊢\", \"⊥\")) # also aligns to bottom-left\n", "\n", "(StSt(\"Text\", fnt, 100,\n", " r=1)) # r=1 reverses direction of the glyphs\n", "\n", "# Variable fonts\n", "\n", "fnt = Font.Find(\"SFNS.ttf\")\n", "\n", "vtxt = (StSt(\"Variable\", fnt, 100,\n", " wght=1, opsz=0)) # maximum weight, minimum optical size\n", "\n", "vtxt = (StSt(\"Variable\", fnt, 100,\n", " wght=0, # minimum weight\n", " opsz=1, # maximum optical size\n", " ro=1, # remove the overlaps (useful for var fonts when applying a stroke)\n", " ))\n", "\n", "# If your variable font has a width axis, you can pass a fit= argument to a StSt constructor in order to have it automatically fit to a given width — here we'll use the included Mutator Sans fitted to the \n", "\n", "fit_txt = (StSt(\"VARIABLE WIDTH\", Font.MutatorSans(), 100,\n", " wdth=1, # fitting always goes from wide to narrow, so make sure to set to max wdth (unless you want it to never be that wide)\n", " fit=r.w-100) # -100 is just some quick padding\n", " .align(r))\n", "\n", "# Multi-line text\n", "# N.B. there is no line-breaking in Coldtype; all line-breaks must be manually done (or you can use drawBot as a package within coldtype to generate multi-line strings that can be vectorized with drawBot.BezierPath)\n", "\n", "txt = (StSt(\"Multi-\\nline\", fnt, 100, \n", " leading=50) # a pixel amount between each line\n", " .align(r))\n", "\n", "txt = (StSt(\"Multi-\\nline\", fnt, 100, \n", " leading=50, xa=\"mnx\") # left align each line\n", " .align(r))\n", "\n", "txt = (StSt(\"Multi-\\nline\", fnt, 100, \n", " leading=50, xa=\"mxx\") # right align each line\n", " .align(r))\n", "\n", "# If you want the text as a single vector (pen), you can do something like this:\n", "\n", "txt = txt.pen()" ] } ], "metadata": { "kernelspec": { "display_name": "venv", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.10.5 (v3.10.5:f377153967, Jun 6 2022, 12:36:10) [Clang 13.0.0 (clang-1300.0.29.30)]" }, "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "10cf79b6252b6bfa5f219a04587890ec267e7f2fde6b173960de4ad2915a3b2e" } } }, "nbformat": 4, "nbformat_minor": 2 } ================================================ FILE: examples/sites/coldtype.goodhertz.com/pages/cheatsheets/viewer.md ================================================ --- title: "Viewer Cheatsheet" --- The most common viewer shortcuts: * ``spacebar`` play the animation * ```` go back one frame * ```` go forward one frame * ``shift+`` go back ten frames * ``shift+`` go forward ten frames * ``a`` render all the frames of the animation to disk * ``cmd+a`` render just the current frame to disk * ``,`` display the rendered version of a frame (and hit ``,`` again to display the live version) — in general this is useful if you want to play the disk-rendered version of an animation (for playing back at correct framerate/speed if animation generation is complex & consequently not realtime) * ``.`` toggle audio support (if you have the ``[audio]`` extra successfully installed) * ```` go to frame 0 * ``q`` quits viewer and kills running coldtype program (equivalent to ctrl-c in command-line or hitting the X in the viewer window) * ``r`` trigger an arbitrary ``release`` function defined in your source * ``b`` trigger an arbitrary ``build`` function defined in your source Utilities: * ``v`` opens the automatic timeline viewer (only applicable for animations, mostly used for MIDI and AsciiTimeline-based animations) * ``p`` prints the current tree * ``u`` loads the next file in the directory (sorted alphabetically) * ``y`` loads the previous file in the directory (sorted alphabetically) * ``s`` shows the current file output directory in the finder ## Command-line window modifications To "pin" the window to corner of your screen, use the window-pin ``-wp`` argument and pass a compass direction (NE, SE, SW, NW), i.e. ``coldtype my-program.py -wp SE`` to have the window appear in the south-east corner of your screen To remove the window background and chrome and make the window itself completely transparent, pass ``-wt 1`` as an argument, i.e. ``coldtype my-program.py -wt 1`` To change the initial scale of the window, pass the preview-scale argument ``-ps 2`` (for example), to get a double-sized window, i.e. ``coldtype my-program.py -ps 2`` To change the opacity of your graphics displayed in the window, pass the window-opacity argument ``-wo 0.5`` (for example), to get a half-transparent window, i.e. ``coldtype my-program.py -wo 0.5`` (All of these arguments can be combined, i.e. a window pinned to the south-west corner of your screen, with no background, 0.75 transparency, and a default size of 0.5x, would be: ``coldtype my-program.py -wt 1 -wp SW -wo 0.75 -ps 0.5``) ================================================ FILE: examples/sites/coldtype.goodhertz.com/pages/classes_functions.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dict(\n", " author=\"Rob Stenson\",\n", " title=\"Classes & Functions\",\n", " date=\"1/7/2023\",\n", " blank=True,\n", ")" ] } ], "metadata": { "kernelspec": { "display_name": "venv", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.10.5 (v3.10.5:f377153967, Jun 6 2022, 12:36:10) [Clang 13.0.0 (clang-1300.0.29.30)]" }, "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "10cf79b6252b6bfa5f219a04587890ec267e7f2fde6b173960de4ad2915a3b2e" } } }, "nbformat": 4, "nbformat_minor": 2 } ================================================ FILE: examples/sites/coldtype.goodhertz.com/pages/install.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dict(\n", " author=\"Rob Stenson\",\n", " date=\"1/6/2023\",\n", " title=\"Install\",\n", ")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "* Install a Python >= 3.7\n", "* If you don’t have a Python >= 3.7, I’d recommend the latest Python (available from [python.org/downloads](https://python.org/downloads)\n", "\n", "If you want to try coldtype in a blank virtual environment:\n", "\n", "Using a virtualenv (based on a python >= 3.7) (aka `python3.10 -m venv venv` and then `source venv/bin/activate`), run:\n", "\n", "* `pip install \"coldtype[viewer]\"`\n", "* `coldtype demo`\n", "\n", "That last command should pop up a window. If you hit the spacebar with that window focused, you should see an animation start playing.\n", "\n", "---\n", "\n", "To write your own script, make a python file in your repo, like *test.py*, and put some code in it, like:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from coldtype import *\n", "\n", "@renderable()\n", "def test(r):\n", " return P().oval(r)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Then you can run that like so — `coldtype test.py` — and a large pink oval should pop up on your screen.\n", "\n", "You may also notice the command is still hanging, meaning it hasn't exited. So if you edit test.py and hit save, you should see the change immediately pop up in the same window. For instance, try insetting the oval and making it a different color, so that your code looks something like this:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from coldtype import *\n", "\n", "@renderable()\n", "def test(r):\n", " return P().oval(r.inset(100)).f(hsl(0.8))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Now the oval should have some padding and be purple-ish.\n", "\n", "To quit the running program, as with all CLI programs, you can hit *ctrl c* and that should kill the program. Or you can type *kill* into the command line while the program is running and it should have the same effect. (That’s not something that’ll usually work, but it works in coldtype.)" ] } ], "metadata": { "kernelspec": { "display_name": "venv", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.10.5 (v3.10.5:f377153967, Jun 6 2022, 12:36:10) [Clang 13.0.0 (clang-1300.0.29.30)]" }, "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "10cf79b6252b6bfa5f219a04587890ec267e7f2fde6b173960de4ad2915a3b2e" } } }, "nbformat": 4, "nbformat_minor": 2 } ================================================ FILE: examples/sites/coldtype.goodhertz.com/pages/introduction.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'author': 'Rob Stenson', 'date': '1/6/2023', 'title': 'Introduction'}" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dict(\n", " author=\"Rob Stenson\",\n", " date=\"1/6/2023\",\n", " title=\"Introduction\",\n", ")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Coldtype\n", "\n", "Coldtype is a cross-platform library for programming and animating display typography with Python.\n", "\n", "Put another way: do you want to make typographic graphics and animations with code? This is a good & idiosyncratic way to do that.\n", "\n", "🌋 **Disclaimer**: coldtype is alpha-quality software 🌋" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "TLDR (if you're using a virtualenv based on python >= 3.7):\n", "\n", "```\n", "pip install \"coldtype[viewer]\"\n", "coldtype demo\n", "```" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Here’s the complete code and the animation it renders:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", " \n", " " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from coldtype import *\n", "from coldtype.notebook import * #hide-publish\n", "\n", "@animation((800, 300), timeline=60, bg=1, render_show=1)\n", "def demo(f):\n", " return (Glyphwise(\"COLDTYPE\", lambda x:\n", " Style(Font.ColdtypeObviously(), 150\n", " , wdth=f.adj(-x.i*15).e(\"eeo\", 1)))\n", " .align(f.a.r)\n", " .f(0))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Here’s an example of a coldtype program:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from coldtype import * #hide-publish\n", "from coldtype.notebook import *\n", "\n", "fnt = Font.ColdtypeObviously()\n", "\n", "@renderable((700, 350))\n", "def coldtype_simple(r):\n", " return (StSt(\"COLDTYPE\", fnt, 350,\n", " wdth=0, tu=20, r=1, rotate=10)\n", " .align(r)\n", " .f(hsl(0.65, 0.6)))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "If you’re familiar with other graphics programming libraries, that code snippet might seem a little odd, given that it’s **canvas-less** & highly **abbreviated**, with a idiomatic emphasis on **method-chaining** & **structured data** (which can make programming **animations** a lot easier and faster). *More on those bold words on the [about](/about.html) page.*" ] } ], "metadata": { "kernelspec": { "display_name": "venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.7" }, "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "10cf79b6252b6bfa5f219a04587890ec267e7f2fde6b173960de4ad2915a3b2e" } } }, "nbformat": 4, "nbformat_minor": 2 } ================================================ FILE: examples/sites/coldtype.goodhertz.com/pages/overview.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dict(\n", " author=\"Rob Stenson\",\n", " title=\"Overview\",\n", " date=\"1/6/2023\",\n", ")" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "#hide-publish\n", "from coldtype.notebook import *" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Anatomy of a Coldtype Program\n", "\n", "When run locally with the coldtype viewer, a coldtype program is a source file, a Python file with a ``*.py`` file extension. Inside that file, you can write anything you want, but at the very least you'll need do two things:\n", "\n", "1. Import the coldtype library\n", "2. Define a renderable function\n", "\n", "So to get going from scratch, create a new Python source file, something like *anatomy.py* or whatever you want to call it.\n", "\n", "Then for the first line:\n", "\n", "```python\n", "from coldtype import *\n", "```\n", "\n", "(You could also do `import coldtype`, but for the examples in this documentation, `from coldtype import *` is the idiomatic import.)\n", "\n", "For the second requirement – **defining a renderable function** — all you need is a function that accepts a single argument, a `Rect`, like so:\n", "\n", "```python\n", "def show_something(r:Rect):\n", " return P().rect(r)\n", "```\n", "\n", "If you followed the install instructions and have activated your virtual environment, you can now run this file from the command-line, like so:\n", "\n", "```\n", "coldtype anatomy.py\n", "```\n", "\n", "Unfortunately, you won't see anything other than a window that pops up and says: \"Nothing found.\"\n", "\n", "That's because we didn't tell the renderer that anything in our source file is `renderable`. We can remedy that situation by adding the `@renderable` decorator right above where we define our rendering function.\n", "\n", "So let’s create a new function and mark it as renderable." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable()\n", "def really_show_something(r:Rect):\n", " return P().rect(r)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Now when you save the source file (no need to stop it and restart it on the command line), you should see a gigantic pink rectangle. If you try changing something about that rectangle, like adding `.f(hsl(random()))` to the end of the return statement, or change the `.rect` to an `.oval`, as soon as you save the source file, you should see changes in the viewer." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable()\n", "def really_show_something(r:Rect):\n", " return P().oval(r).f(hsl(0.3))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "What about text?" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((800, 300))\n", "def sample_text(r):\n", " return P(\n", " P().oval(r.inset(20)).f(hsl(random())),\n", " StSt(\"COLDTYPE\",\n", " Font.ColdtypeObviously(), 200,\n", " wdth=0, tu=100, rotate=10)\n", " .align(r)\n", " .f(1))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "(Some of that text-setting code might seem a little bewildering, but all of it’s covered in the Text tutorial in this documentation.)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Workflow of a Coldtype Program\n", "\n", "At this point you might be wondering, *how do I save what's on screen to a file on my computer?*\n", "\n", "If you’re familiar with DrawBot, you might think you need to write some code to do that, but one of the core tenets of coldtype is that rendering and rasterizing are handled by the renderer, not by the source code. This makes some things that are easy in DrawBot difficult in Coldtype, but it also makes many things that are difficult in DrawBot extremely easy in Coldtype.\n", "\n", "If you have your program running, to render what you have so far to disk, you can focus on the Coldtype viewer window and hit the `a` key on your keyboard. That should print some things out on the command-line, including the path of a brand-new png file.\n", "\n", "A few notes:\n", "\n", "* The name of the file created is the name of the source file combined with the name of the function. So in this case, the `sample_text` function rendered to a png called `renders/overview_sample_text.png`. The `renders` folder is automatically created by the renderer. (All of this can be customized, but this is the default behavior for all non-animation graphics.)\n", "\n", "* `a` stands for \"all,\" as in `render-all`. In Coldtype there is a distinction between rendering all of what a file represents, versus just a segment (workarea) of what a file represents. If you're coding a small number of static graphics like the pink rectangle in our source file, the distinction between render-all and render-workarea is basically meaningless, because it takes such a short amount of time to render everything. But when you’re working on an animation — when you’re rendering 3000 frames and each frame takes a few milliseconds to render — the distinction becomes crucial." ] } ], "metadata": { "kernelspec": { "display_name": "venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.5" }, "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "10cf79b6252b6bfa5f219a04587890ec267e7f2fde6b173960de4ad2915a3b2e" } } }, "nbformat": 4, "nbformat_minor": 2 } ================================================ FILE: examples/sites/coldtype.goodhertz.com/pages/tutorials/animation.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dict(\n", " author=\"Rob Stenson\",\n", " title=\"Animation\",\n", " date=\"11/29/2022\"\n", ")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "id": "QC6cYncZTbSM" }, "source": [ "#### __N.B.__\n", "\n", "There are lots of examples of somewhat complex animation in the [examples/animations](https://github.com/goodhertz/coldtype/tree/main/examples/animations>) folder in the coldtype repository, but here are some simpler (and shorter) ones, that demonstrate the fundamentals of how animations are built in coldtype." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "id": "vQqKKBu6fSZh" }, "outputs": [], "source": [ "#hide-publish\n", "%pip install -q coldtype[notebook]\n", "#!pip install -q \"coldtype[notebook] @ git+https://github.com/goodhertz/coldtype\"\n", "from coldtype.notebook import *" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "id": "CwcUbsLlm-dr" }, "source": [ "## A circle moving" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 323, "referenced_widgets": [ "bfdb2e3af11e4420aa5fee146015a813", "ba7d48de62b54b328aded13f6a33db50", "633e32188b994141ace0d5a191307168", "560eba413abb4bddbe32ecbbbecd2216", "44a8dcc25bf1472cbf16394402c8d2e9", "06f952bd1d01459884e9fb9667f99b29", "49c00b6f011942fea03698c27a3f6780" ] }, "id": "A8_pGQ6vfe7j", "outputId": "70ff1076-ea0e-415b-a548-70f48b768714" }, "outputs": [ { "data": { "text/html": [ "\n", " \n", " " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@animation((540, 540), timeline=60, bg=1, render_show=1)\n", "def circle(f):\n", " return (P().oval(f.a.r.inset(120)\n", " .offset(f.e(\"eei\", 1, rng=(-f.a.r.w/2, f.a.r.w/2)), 0))\n", " .f(hsl(0.7)))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "id": "ckjOCzI3np1A" }, "source": [ "## A letter flying" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 323, "referenced_widgets": [ "04861d0eaee24b78a8bfceb61f2a73f8", "96ca4d76d285483686346f2cf65eb04f", "20edb4cc9ab24fa69dbcab8dc822a704", "db671e01989642d4860294466cf1a3de", "a537f66528c74e239f98d73ed4d7693e", "d9b1902acbf442d6a4bd223b7bf6bd08", "6e6e06c1613141ba9a182c038076aa3d" ] }, "id": "CH7Y341Ffr_M", "outputId": "b2e59956-9279-4abd-d11c-f6692d35e1fe" }, "outputs": [ { "data": { "text/html": [ "\n", " \n", " " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@animation((540, 540), bg=0, timeline=24, render_show=1)\n", "def flying(f):\n", " return P(\n", " P().rect(f.a.r)\n", " .f(hsl(f.e(\"qeio\", 0))),\n", " StSt(\"A\", Font.MutatorSans(),\n", " 50, wght=0.2)\n", " .align(f.a.r)\n", " .scale(f.e(\"eei\", 0, rng=(1, 120)))\n", " .rotate(f.e(\"qeio\", 0, rng=(0, 360)))\n", " .f(1))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "id": "yl-jQtUqqiGU" }, "source": [ "## Simple variation" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 323, "referenced_widgets": [ "437ab795916e455385b0490a5cbb141e", "cb6e231ea4ec42fa81f58c9543577a64", "ac336c4a53bb416eb5ca9d80cd510afe", "69ef1749f6ee433688087f0faba7d7bd", "4dc0a2c7c57c401baa4feb035166eaff", "a47e01d7757e42ebb4d194c998683c36", "8847a2a280b840198999a43a90c43e7a" ] }, "id": "KUH3FPI0qHbN", "outputId": "70491080-3e9e-453d-bf4d-837f4fdd36c5" }, "outputs": [ { "data": { "text/html": [ "\n", " \n", " " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@animation((1080, 540), timeline=50, bg=hsl(0.4), render_show=True)\n", "def vari(f):\n", " return (StSt(\"CDELOPTY\",\n", " Font.ColdtypeObviously(), 200,\n", " wdth=f.e(\"eeio\", 1))\n", " .align(f.a.r)\n", " .f(1))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "id": "QvTOQcXTp8Dh" }, "source": [ "## A variable wave" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 323, "referenced_widgets": [ "c75724b103154e1eb9ada5bd8089ffed", "6a31dbb7de73486e8a788d1c14ff257a", "1b94fc58721349d2b61146a70d7b0944", "d2d5a6f4271f4c0dabed5784647c110b", "cd5b60363e984fa680dff5204402d4ac", "ea7aeb9bd43940a2aa9aa1fe0e63bcc6", "0f6f2c236fdc486b811f8c0c56e3ad7b" ] }, "id": "4E1F649RoK4C", "outputId": "3e45a785-1499-4722-a830-f4f94f92ed13" }, "outputs": [ { "data": { "text/html": [ "\n", " \n", " " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@animation((1080, 540), timeline=50, bg=hsl(0.6), render_show=1)\n", "def wave(f):\n", " return (Glyphwise(\"COLDTYPE\", lambda g:\n", " Style(Font.ColdtypeObviously(), 200,\n", " wdth=f.adj(-g.i*3).e(\"seio\", 1)))\n", " .align(f.a.r)\n", " .f(1))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "id": "NgNsQgpuS_Hk" }, "source": [ "## Note on running Coldtype locally\n", "\n", "To generate a full set of frames for a coldtype animation on your local computer (as opposed to here on Colab), hit the `a` key in the viewer app — once you do, you should see the command line prompt printing out a bunch of information about frames being rendered. (Also, once you do that, you can hit `shift+space` to preview the animation in real time at the correct frame rate, using the cached frames.)" ] } ], "metadata": { "colab": { "collapsed_sections": [], "name": "Coldtype Animation Tutorial.ipynb", "provenance": [] }, "kernelspec": { "display_name": "Python 3.10.5 ('venv': venv)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.5" }, "vscode": { "interpreter": { "hash": "10cf79b6252b6bfa5f219a04587890ec267e7f2fde6b173960de4ad2915a3b2e" } }, "widgets": { "application/vnd.jupyter.widget-state+json": { "02a14a2d803346758db76bd32a576180": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "HTMLModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "HTMLModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "HTMLView", "description": "", "description_tooltip": null, "layout": "IPY_MODEL_5f8f603c45754f219d151f8afdb13e5d", "placeholder": "​", "style": "IPY_MODEL_d8ca640546dc4f67901782e647a3d518", "value": " 49/50 [00:03<00:00, 14.18it/s]" } }, "04861d0eaee24b78a8bfceb61f2a73f8": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "VBoxModel", "state": { "_dom_classes": [ "widget-interact" ], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "VBoxModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "VBoxView", "box_style": "", "children": [ "IPY_MODEL_96ca4d76d285483686346f2cf65eb04f", "IPY_MODEL_20edb4cc9ab24fa69dbcab8dc822a704" ], "layout": "IPY_MODEL_db671e01989642d4860294466cf1a3de" } }, "067c65a794b342cc94c879508bcdb3c2": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "ProgressStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "ProgressStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "StyleView", "bar_color": null, "description_width": "" } }, "06f952bd1d01459884e9fb9667f99b29": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "SliderStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "SliderStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "StyleView", "description_width": "", "handle_color": null } }, "0aff7a198fbd457794be9368c03c5720": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "0f6f2c236fdc486b811f8c0c56e3ad7b": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "197e3308b173413f90434ab3516242c6": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "HTMLModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "HTMLModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "HTMLView", "description": "", "description_tooltip": null, "layout": "IPY_MODEL_c6bb573528334cd3be0d344b9fce1728", "placeholder": "​", "style": "IPY_MODEL_be860646020445dfbc6878ce5f3da130", "value": " 49/50 [00:02<00:00, 19.50it/s]" } }, "1b94fc58721349d2b61146a70d7b0944": { "model_module": "@jupyter-widgets/output", "model_module_version": "1.0.0", "model_name": "OutputModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/output", "_model_module_version": "1.0.0", "_model_name": "OutputModel", "_view_count": null, "_view_module": "@jupyter-widgets/output", "_view_module_version": "1.0.0", "_view_name": "OutputView", "layout": "IPY_MODEL_0f6f2c236fdc486b811f8c0c56e3ad7b", "msg_id": "", "outputs": [ { "data": { "text/html": "", "text/plain": "" }, "metadata": {}, "output_type": "display_data" } ] } }, "1c82e07be30542a395dba7927cfa729c": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "20edb4cc9ab24fa69dbcab8dc822a704": { "model_module": "@jupyter-widgets/output", "model_module_version": "1.0.0", "model_name": "OutputModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/output", "_model_module_version": "1.0.0", "_model_name": "OutputModel", "_view_count": null, "_view_module": "@jupyter-widgets/output", "_view_module_version": "1.0.0", "_view_name": "OutputView", "layout": "IPY_MODEL_6e6e06c1613141ba9a182c038076aa3d", "msg_id": "", "outputs": [ { "data": { "text/html": "", "text/plain": "" }, "metadata": {}, "output_type": "display_data" } ] } }, "314f2b895cd14b1db927c05420430202": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "DescriptionStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "DescriptionStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "StyleView", "description_width": "" } }, "3c9a95ff656f47a9945eedadaf018a0d": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "DescriptionStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "DescriptionStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "StyleView", "description_width": "" } }, "3f0d562e20494f169b1f14682aced177": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "HTMLModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "HTMLModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "HTMLView", "description": "", "description_tooltip": null, "layout": "IPY_MODEL_74030c01b4964bc091ec673b48278993", "placeholder": "​", "style": "IPY_MODEL_3c9a95ff656f47a9945eedadaf018a0d", "value": " 97%" } }, "422e00cbb23546faa5c065c9e13e83eb": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "DescriptionStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "DescriptionStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "StyleView", "description_width": "" } }, "43308ade94d448c0aaf5bd71b75a9fed": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "437ab795916e455385b0490a5cbb141e": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "VBoxModel", "state": { "_dom_classes": [ "widget-interact" ], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "VBoxModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "VBoxView", "box_style": "", "children": [ "IPY_MODEL_cb6e231ea4ec42fa81f58c9543577a64", "IPY_MODEL_ac336c4a53bb416eb5ca9d80cd510afe" ], "layout": "IPY_MODEL_69ef1749f6ee433688087f0faba7d7bd" } }, "44a8dcc25bf1472cbf16394402c8d2e9": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "49c00b6f011942fea03698c27a3f6780": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "4cbb64a73c374eb18a6dd48b338051c2": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "4dc0a2c7c57c401baa4feb035166eaff": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "4e431b5a4e474bd7b3f72b4c660c2711": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "ProgressStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "ProgressStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "StyleView", "bar_color": null, "description_width": "" } }, "53aa89214ad74b078fc5b877cfd085e8": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "HTMLModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "HTMLModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "HTMLView", "description": "", "description_tooltip": null, "layout": "IPY_MODEL_e20317954dd94a2689c581589aab0c21", "placeholder": "​", "style": "IPY_MODEL_f6c9fd24875a42fe9846adbc42ada232", "value": "100%" } }, "55d05667c1ea44b78281c8cdaa7d5b6f": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "HTMLModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "HTMLModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "HTMLView", "description": "", "description_tooltip": null, "layout": "IPY_MODEL_9eb8b3259aa04b5089e688ea1649458d", "placeholder": "​", "style": "IPY_MODEL_422e00cbb23546faa5c065c9e13e83eb", "value": " 98%" } }, "560eba413abb4bddbe32ecbbbecd2216": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "5a52b2c9de8149ab96d47eac2361c8b1": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "ProgressStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "ProgressStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "StyleView", "bar_color": null, "description_width": "" } }, "5f8f603c45754f219d151f8afdb13e5d": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "633e32188b994141ace0d5a191307168": { "model_module": "@jupyter-widgets/output", "model_module_version": "1.0.0", "model_name": "OutputModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/output", "_model_module_version": "1.0.0", "_model_name": "OutputModel", "_view_count": null, "_view_module": "@jupyter-widgets/output", "_view_module_version": "1.0.0", "_view_name": "OutputView", "layout": "IPY_MODEL_49c00b6f011942fea03698c27a3f6780", "msg_id": "", "outputs": [ { "data": { "text/html": "", "text/plain": "" }, "metadata": {}, "output_type": "display_data" } ] } }, "69ef1749f6ee433688087f0faba7d7bd": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "6a24d7d5acd340768e625fb7fbc239fe": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "HBoxModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "HBoxModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "HBoxView", "box_style": "", "children": [ "IPY_MODEL_d300c9571b214fbcbe68c7f8a93d3119", "IPY_MODEL_7a2889dc6ad54326892560057f177caf", "IPY_MODEL_197e3308b173413f90434ab3516242c6" ], "layout": "IPY_MODEL_a115c437fb394320b0da2cb7163cfdb0" } }, "6a31dbb7de73486e8a788d1c14ff257a": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "IntSliderModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "IntSliderModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "IntSliderView", "continuous_update": false, "description": "f.i", "description_tooltip": null, "disabled": false, "layout": "IPY_MODEL_cd5b60363e984fa680dff5204402d4ac", "max": 49, "min": 0, "orientation": "horizontal", "readout": true, "readout_format": "d", "step": 1, "style": "IPY_MODEL_ea7aeb9bd43940a2aa9aa1fe0e63bcc6", "value": 0 } }, "6e6e06c1613141ba9a182c038076aa3d": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "74030c01b4964bc091ec673b48278993": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "77c5bf8c053d477c888f6717bda13cfa": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "HTMLModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "HTMLModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "HTMLView", "description": "", "description_tooltip": null, "layout": "IPY_MODEL_d44bb7280c134550bef865aae512f4c6", "placeholder": "​", "style": "IPY_MODEL_d1ddc4ef041f4621b4296af8833d9f20", "value": " 24/24 [00:00<00:00, 37.78it/s]" } }, "7a2889dc6ad54326892560057f177caf": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "FloatProgressModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "FloatProgressModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "ProgressView", "bar_style": "", "description": "", "description_tooltip": null, "layout": "IPY_MODEL_43308ade94d448c0aaf5bd71b75a9fed", "max": 50, "min": 0, "orientation": "horizontal", "style": "IPY_MODEL_5a52b2c9de8149ab96d47eac2361c8b1", "value": 50 } }, "8847a2a280b840198999a43a90c43e7a": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "888cfb6ab4464a8e9770167948beaba7": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "FloatProgressModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "FloatProgressModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "ProgressView", "bar_style": "", "description": "", "description_tooltip": null, "layout": "IPY_MODEL_1c82e07be30542a395dba7927cfa729c", "max": 24, "min": 0, "orientation": "horizontal", "style": "IPY_MODEL_067c65a794b342cc94c879508bcdb3c2", "value": 24 } }, "8ccc9578378f4c9ab705f74788d596d1": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "941b6cfd0beb474288cbe7ec85301dab": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "FloatProgressModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "FloatProgressModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "ProgressView", "bar_style": "", "description": "", "description_tooltip": null, "layout": "IPY_MODEL_0aff7a198fbd457794be9368c03c5720", "max": 60, "min": 0, "orientation": "horizontal", "style": "IPY_MODEL_4e431b5a4e474bd7b3f72b4c660c2711", "value": 60 } }, "96ca4d76d285483686346f2cf65eb04f": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "IntSliderModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "IntSliderModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "IntSliderView", "continuous_update": false, "description": "f.i", "description_tooltip": null, "disabled": false, "layout": "IPY_MODEL_a537f66528c74e239f98d73ed4d7693e", "max": 23, "min": 0, "orientation": "horizontal", "readout": true, "readout_format": "d", "step": 1, "style": "IPY_MODEL_d9b1902acbf442d6a4bd223b7bf6bd08", "value": 23 } }, "981e9cd4af5b4e119115f5b8911c5c24": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "9c27177f679f41e899d697ab7816a107": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "ProgressStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "ProgressStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "StyleView", "bar_color": null, "description_width": "" } }, "9eb8b3259aa04b5089e688ea1649458d": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "a115c437fb394320b0da2cb7163cfdb0": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "a47e01d7757e42ebb4d194c998683c36": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "SliderStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "SliderStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "StyleView", "description_width": "", "handle_color": null } }, "a537f66528c74e239f98d73ed4d7693e": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "ac336c4a53bb416eb5ca9d80cd510afe": { "model_module": "@jupyter-widgets/output", "model_module_version": "1.0.0", "model_name": "OutputModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/output", "_model_module_version": "1.0.0", "_model_name": "OutputModel", "_view_count": null, "_view_module": "@jupyter-widgets/output", "_view_module_version": "1.0.0", "_view_name": "OutputView", "layout": "IPY_MODEL_8847a2a280b840198999a43a90c43e7a", "msg_id": "", "outputs": [ { "data": { "text/html": "", "text/plain": "" }, "metadata": {}, "output_type": "display_data" } ] } }, "ae7906dad19d4e5a8dd98bb089fcf174": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "HBoxModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "HBoxModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "HBoxView", "box_style": "", "children": [ "IPY_MODEL_3f0d562e20494f169b1f14682aced177", "IPY_MODEL_941b6cfd0beb474288cbe7ec85301dab", "IPY_MODEL_dd6bba0ccbed4a2e957d6410c3a46cf0" ], "layout": "IPY_MODEL_4cbb64a73c374eb18a6dd48b338051c2" } }, "b127b473785145859c09a4252584e587": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "b910bfa8acaa4a51af94cc7d38ff9d13": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "ba7d48de62b54b328aded13f6a33db50": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "IntSliderModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "IntSliderModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "IntSliderView", "continuous_update": false, "description": "f.i", "description_tooltip": null, "disabled": false, "layout": "IPY_MODEL_44a8dcc25bf1472cbf16394402c8d2e9", "max": 59, "min": 0, "orientation": "horizontal", "readout": true, "readout_format": "d", "step": 1, "style": "IPY_MODEL_06f952bd1d01459884e9fb9667f99b29", "value": 0 } }, "be860646020445dfbc6878ce5f3da130": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "DescriptionStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "DescriptionStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "StyleView", "description_width": "" } }, "bfdb2e3af11e4420aa5fee146015a813": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "VBoxModel", "state": { "_dom_classes": [ "widget-interact" ], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "VBoxModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "VBoxView", "box_style": "", "children": [ "IPY_MODEL_ba7d48de62b54b328aded13f6a33db50", "IPY_MODEL_633e32188b994141ace0d5a191307168" ], "layout": "IPY_MODEL_560eba413abb4bddbe32ecbbbecd2216" } }, "c6bb573528334cd3be0d344b9fce1728": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "c75724b103154e1eb9ada5bd8089ffed": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "VBoxModel", "state": { "_dom_classes": [ "widget-interact" ], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "VBoxModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "VBoxView", "box_style": "", "children": [ "IPY_MODEL_6a31dbb7de73486e8a788d1c14ff257a", "IPY_MODEL_1b94fc58721349d2b61146a70d7b0944" ], "layout": "IPY_MODEL_d2d5a6f4271f4c0dabed5784647c110b" } }, "cb6e231ea4ec42fa81f58c9543577a64": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "IntSliderModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "IntSliderModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "IntSliderView", "continuous_update": false, "description": "f.i", "description_tooltip": null, "disabled": false, "layout": "IPY_MODEL_4dc0a2c7c57c401baa4feb035166eaff", "max": 49, "min": 0, "orientation": "horizontal", "readout": true, "readout_format": "d", "step": 1, "style": "IPY_MODEL_a47e01d7757e42ebb4d194c998683c36", "value": 0 } }, "cd5b60363e984fa680dff5204402d4ac": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "cd94402037b44ec6bc759085d156dcde": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "DescriptionStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "DescriptionStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "StyleView", "description_width": "" } }, "d1ddc4ef041f4621b4296af8833d9f20": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "DescriptionStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "DescriptionStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "StyleView", "description_width": "" } }, "d2d5a6f4271f4c0dabed5784647c110b": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "d300c9571b214fbcbe68c7f8a93d3119": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "HTMLModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "HTMLModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "HTMLView", "description": "", "description_tooltip": null, "layout": "IPY_MODEL_ddf633f6286d47e1bd2e8f16dadaa9e7", "placeholder": "​", "style": "IPY_MODEL_314f2b895cd14b1db927c05420430202", "value": " 98%" } }, "d44bb7280c134550bef865aae512f4c6": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "d577385865e24560a43faf19daabd25c": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "HBoxModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "HBoxModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "HBoxView", "box_style": "", "children": [ "IPY_MODEL_53aa89214ad74b078fc5b877cfd085e8", "IPY_MODEL_888cfb6ab4464a8e9770167948beaba7", "IPY_MODEL_77c5bf8c053d477c888f6717bda13cfa" ], "layout": "IPY_MODEL_b127b473785145859c09a4252584e587" } }, "d6be721629d144709382f06237dbe853": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "FloatProgressModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "FloatProgressModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "ProgressView", "bar_style": "", "description": "", "description_tooltip": null, "layout": "IPY_MODEL_b910bfa8acaa4a51af94cc7d38ff9d13", "max": 50, "min": 0, "orientation": "horizontal", "style": "IPY_MODEL_9c27177f679f41e899d697ab7816a107", "value": 50 } }, "d8ca640546dc4f67901782e647a3d518": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "DescriptionStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "DescriptionStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "StyleView", "description_width": "" } }, "d9b1902acbf442d6a4bd223b7bf6bd08": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "SliderStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "SliderStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "StyleView", "description_width": "", "handle_color": null } }, "db671e01989642d4860294466cf1a3de": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "dd6bba0ccbed4a2e957d6410c3a46cf0": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "HTMLModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "HTMLModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "HTMLView", "description": "", "description_tooltip": null, "layout": "IPY_MODEL_8ccc9578378f4c9ab705f74788d596d1", "placeholder": "​", "style": "IPY_MODEL_cd94402037b44ec6bc759085d156dcde", "value": " 58/60 [00:02<00:00, 23.74it/s]" } }, "ddf633f6286d47e1bd2e8f16dadaa9e7": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "e20317954dd94a2689c581589aab0c21": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.2.0", "model_name": "LayoutModel", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", "_model_name": "LayoutModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "LayoutView", "align_content": null, "align_items": null, "align_self": null, "border": null, "bottom": null, "display": null, "flex": null, "flex_flow": null, "grid_area": null, "grid_auto_columns": null, "grid_auto_flow": null, "grid_auto_rows": null, "grid_column": null, "grid_gap": null, "grid_row": null, "grid_template_areas": null, "grid_template_columns": null, "grid_template_rows": null, "height": null, "justify_content": null, "justify_items": null, "left": null, "margin": null, "max_height": null, "max_width": null, "min_height": null, "min_width": null, "object_fit": null, "object_position": null, "order": null, "overflow": null, "overflow_x": null, "overflow_y": null, "padding": null, "right": null, "top": null, "visibility": null, "width": null } }, "ea7aeb9bd43940a2aa9aa1fe0e63bcc6": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "SliderStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "SliderStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "StyleView", "description_width": "", "handle_color": null } }, "f6c9fd24875a42fe9846adbc42ada232": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "DescriptionStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "DescriptionStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "1.2.0", "_view_name": "StyleView", "description_width": "" } }, "ff696df1e2b74b5f9fd871d850e0c6f9": { "model_module": "@jupyter-widgets/controls", "model_module_version": "1.5.0", "model_name": "HBoxModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", "_model_name": "HBoxModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "1.5.0", "_view_name": "HBoxView", "box_style": "", "children": [ "IPY_MODEL_55d05667c1ea44b78281c8cdaa7d5b6f", "IPY_MODEL_d6be721629d144709382f06237dbe853", "IPY_MODEL_02a14a2d803346758db76bd32a576180" ], "layout": "IPY_MODEL_981e9cd4af5b4e119115f5b8911c5c24" } } } } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: examples/sites/coldtype.goodhertz.com/pages/tutorials/blender.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'author': 'Rob Stenson', 'title': 'Blender', 'date': '1/9/2023'}" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dict(\n", " author=\"Rob Stenson\",\n", " title=\"Blender\",\n", " date=\"1/9/2023\",\n", ")" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "#hide-publish\n", "from coldtype.notebook import *" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Behind the scenes, Coldtype uses the Skia library to rasterize two-dimensional vectors. But what if we want to rasterize *three-dimensional* graphics? One option is to use Blender." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "Because Blender has an incredible Python API, it's not too difficult to use it programmatically — i.e. to write a normal Coldtype script, mark a few things (with metadata specific to Blender), and then let Blender & Coldtype take care of the translation to three dimensions. Here's an example:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "from coldtype.blender import *\n", "\n", "fnt = Font.Find(\"SwearCilatiVariable\")\n", "\n", "@b3d_animation(timeline=60)\n", "def varfont(f):\n", " return (Glyphwise(\"Vari\", lambda g:\n", " Style(fnt, 325,\n", " opsz=f.adj(-g.i*5).e(\"seio\", 1, rng=(0.98, 0)),\n", " wght=f.adj(-g.i*15).e(\"seio\", 1, rng=(0.98, 0))\n", " ))\n", " .align(f.a.r)\n", " .mapv(lambda i, p: p\n", " .ch(b3d(lambda bp: bp\n", " .extrude(f.adj(-i*5)\n", " .e(\"ceio\", 1, rng=(0.015, 3)))))))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Running Code in Blender\n", "\n", "First off, your virtual environment needs to be built with the same version of Python as the Blender version you’re targeting.\n", "\n", "Behind the scenes, Coldtype uses a tool called `b3denv` to communicate with Blender, and `b3denv` — installed as a dependency of Coldtype — makes it easy to create a virtual environment using the Python bundled with Blender.\n", "\n", "```bash\n", "b3denv python -m venv benv\n", "```\n", "\n", "Now you’ll have a blender-specific virtual environment named `benv`, which you can activate the usual way, `source benv/scripts/activate` on Mac.\n", "\n", "To get a Blender window to show up, all you need to do is use the ``@b3d_animation`` decorator in place of the standard @animation decorator, and add ``-bw 1`` to the command-line invocation. Or, if you want a set of sensible CLI defaults, try ``-p b3d`` instead, which stands for ``--profile=b3d`` and sets ``-bw 1`` as part of some other settings in the b3d profile.\n", "\n", "So, to use an example from the Coldtype repo, you could save the code from above and run:\n", "\n", "```bash\n", "coldtype examples/blender/varfont.py -p b3d\n", "```\n", "\n", "This should launch both a standard Coldtype window (with a 2D Skia renderer) and a Blender GUI window, which should automatically render the same thing as the 2D window, except in 3D. Put another way: you do not need to open Blender yourself, since Coldtype launches it as a background process (necessary to connect the live-code-reloading part of Coldtype to Blender). To quit both Coldtype and Blender, just hit ctrl-c in the terminal.\n", "\n", "What's different in Blender is that the contents of the scene aren’t re-created from scratch every time you render; instead, you annotate specific elements in your returned result, then those annotated results are displayed in Blender, as persistent objects. This means you can use Blender in a hybrid fashion, creating objects using the GUI, saving the file, and then re-saving your Coldtype source file for automatic updates in Blender itself.\n", "\n", "If you’d like to skip seeing the 2D Coldtype window, you can use `--b3dlo` instead. " ] } ], "metadata": { "kernelspec": { "display_name": "venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.5 (v3.10.5:f377153967, Jun 6 2022, 12:36:10) [Clang 13.0.0 (clang-1300.0.29.30)]" }, "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "10cf79b6252b6bfa5f219a04587890ec267e7f2fde6b173960de4ad2915a3b2e" } } }, "nbformat": 4, "nbformat_minor": 2 } ================================================ FILE: examples/sites/coldtype.goodhertz.com/pages/tutorials/drawbot.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dict(\n", " author=\"Rob Stenson\",\n", " title=\"DrawBot\",\n", " date=\"1/6/2023\"\n", ")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Drawbot-in-Coldtype\n", "\n", "Though [DrawBot](https://drawbot.com) and Coldtype encourage different programming styles, DrawBot can be used inside Coldtype, by using a special type of renderable — the `@drawbot_script` renderable.\n", "\n", "### Installing\n", "\n", "DrawBot is not installed by default with Coldtype, so you’ll need to install DrawBot in your virtualenv, like so:\n", "\n", "```\n", "pip install git+https://github.com/typemytype/drawbot\n", "```\n", "\n", "Now that you’ve got the module version of DrawBot installed, with just a little bit of preamble and the `@drawbot_script` renderable, you can now use Coldtype to do anything you'd normally do in a DrawBot script." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from coldtype.drawbot import *\n", "from drawBot import *\n", "\n", "@drawbot_renderable((500, 300))\n", "def db_text(r):\n", " fontSize(100)\n", " text(\"Hello!\", (50, 50))\n", "\n", "db_text.notebook_display() #hide-publish" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Unlike other renderables, the `@drawbot_renderable` renderable is “self-rasterizing,” meaning what it communicates to the renderer is not data about what to draw, but the path to a pre-baked image. The details of that process aren't that important, but the takeaway is that this is pretty close to a normal DrawBot programming session, with the caveat that when you zoom in and out in the viewer, the script must re-render completely, because what you’re seeing is an image (and not a PDF like you see the DrawBot app). (More about zooming down below in \"Scaling.\")\n", "\n", "Another caveat is that drawBot's ``newPage`` and ``size`` functions won’t work, as the dimensions of a graphic must be passed to the ``@drawbot_renderable`` decorator, ala ``@drawbot_renderable((500, 500))`` if you wanted a 500px x 500px graphic. (More down below on why ``newPage`` doesn’t make sense in Coldtype.)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Combining Idioms\n", "\n", "You might be wondering why you’d want to use DrawBot in Coldtype. To me, one big upside is being able to use any text editor you want, rather than the DrawBot app itself." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import coldtype.drawbot as ctdb\n", "from drawBot import *\n", "\n", "long_txt1 = \"Here is a long string which needs line-breaks to be typeset correctly — something Coldtype can’t do but DrawBot (by leveraging the CoreText APIs on macOS) can handle with aplomb.\"\n", " \n", "long_txt2 = \"Here is another long string, this time set into an oval, made possible by sending textBox a BezierPath generated from a coldtype P via the tobp method available in the coldtype.drawbot helpers module.\"\n", "\n", "@drawbot_script((500, 500))\n", "def combined_idioms(r):\n", " fontSize(24)\n", " textBox(long_txt1, r.inset(10))\n", " # Coldtype Rect's can be passed anywhere a rectangle-like list would be passed in DrawBot\n", "\n", " oval = (P()\n", " .oval(r.take(0.75, \"mny\")\n", " .inset(20).square()))\n", "\n", " (oval.copy()\n", " .outline(20)\n", " .f(hsl(0.95, 1, 0.8, a=0.25))\n", " .chain(ctdb.dbdraw))\n", " \n", " textBox(long_txt2,\n", " ctdb.tobp(oval), align=\"right\")\n", "\n", "combined_idioms.notebook_display() #hide-publish" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Multi-page documents\n", "\n", "In general, Coldtype does not support the idea of a multi-page document; the closest thing supported natively by Coldtype is an `@animation` renderable — and if you think about it, what’s the real difference between a multi-frame animation and a multi-page document? Luckily there’s a `@drawbot_animation` renderable that makes multi-frame drawBot animations very easy.\n", "\n", "All that said, it is still quite possible to do normal DrawBot things in a Coldtype script. So here’s an example of generating a multi-page PDF, using a combination of Coldtype and DrawBot constructs." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "from coldtype.drawbot import *\n", "from drawBot import *\n", "\n", "@drawbot_animation((500, 200))\n", "def multipage_doc(f):\n", " c = hsl(f.e(\"l\", 0), s=0.5, l=0.5)\n", " (P(f.a.r)\n", " .f(c)\n", " .chain(ctdb.dbdraw))\n", " fontSize(50)\n", " fill(1)\n", " textBox(\"Page \" + str(f.i), f.a.r.inset(50))\n", "\n", "\n", "def release(passes):\n", " ctdb.pdfdoc(multipage_doc,\n", " \"examples/drawbot/drawbot_multipage.pdf\")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "The key to making the above work is the magic function `release`, which can be defined once in any Coldtype source file, and provides a \"second chance\" to create artifacts based on what's been rendered by the coldtype renderer. The salient point here is that you can write your own special code to run whenever the `release` action is called, which can be outside the standard save/reload/render workflow of Coldtype. This can be useful for all kinds of things (it’s how this documentation is generated, for example), but here it's useful because we're saying, *OK, the graphics look good, let's now use DrawBot to bake a PDF, using the same code that we've been editing and previewing via the Coldtype viewer.*\n", "\n", "How to trigger the release code? It’s as easy as hitting the R key with the viewer app focused." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Scaling\n", "\n", "Because the default behavior of DrawBot is to display a PDF of the result of your code and to zoom in on a composition automatically, you might be surprised that graphics appear pretty small in the Coldtype viewer window by default, because Coldtype defaults to showing the graphics at their actual size. If you'd like to default to showing your graphics at a higher resolution (i.e. if you’re making a PDF), there are a few options:\n", "\n", "* You can zoom in with +/- on your keyboard in the viewer app\n", "\n", "* You can specify a `preview-scale` argument to the renderer itself when you start it on the command-line, ala `coldtype drawbot_script.py -ps 2`" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Coldtype-in-DrawBot\n", "\n", "If you’re used to Coldtype idioms but want to use DrawBot, you can install the core functionality of Coldtype in DrawBot and use it like a normal Python package.\n", "\n", "To install Coldtype in DrawBot, open up DrawBot and then navigate via the top bar to Python > Install Python Packages. There you can switch the input selector to \"Install\" and then type in \"coldtype\".\n", "\n", "Now you should be able to access Coldtype-in-DrawBot, like so:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from coldtype.drawbot import *\n", "\n", "r = page_rect()\n", "fp = \"/System/Library/Fonts/SFCompactRounded.ttf\"\n", "f = Font.Cacheable(fp)\n", "\n", "(StSt(\"Coldtype\", f, 200, r, wght=1)\n", " .f(hsl(0.8))\n", " .align(r)\n", " .chain(dbdraw))" ] } ], "metadata": { "kernelspec": { "display_name": "venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.5 (v3.10.5:f377153967, Jun 6 2022, 12:36:10) [Clang 13.0.0 (clang-1300.0.29.30)]" }, "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "10cf79b6252b6bfa5f219a04587890ec267e7f2fde6b173960de4ad2915a3b2e" } } }, "nbformat": 4, "nbformat_minor": 2 } ================================================ FILE: examples/sites/coldtype.goodhertz.com/pages/tutorials/geometry.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dict(\n", " author=\"Rob Stenson\",\n", " title=\"Geometry\",\n", " date=\"11/29/2022\"\n", ")" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "id": "vQqKKBu6fSZh" }, "outputs": [], "source": [ "#hide-publish\n", "%pip install -q coldtype[notebook]\n", "#!pip install -q \"coldtype[notebook] @ git+https://github.com/goodhertz/coldtype\"\n", "from coldtype.notebook import *" ] }, { "cell_type": "markdown", "metadata": { "id": "CwcUbsLlm-dr" }, "source": [ "## Dividing Rect(angles)\n", "\n", "One of the core concepts of Coldtype is the use of the `coldtype.geometry.Rect` class to encapsulate rectangles and methods for slicing & dicing them.\n", "\n", "The most basic rectangle is the one passed to a `renderable`, i.e. the `r` variable you get when you define a renderable function, like `def r1(r)` below. So to fill the entire canvas with a single random color, you can do something like this:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 171 }, "id": "A8_pGQ6vfe7j", "outputId": "b1e522a5-3098-40e5-c18b-7d62f99ed524" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((700, 300))\n", "def r1(r):\n", " return P(r).f(hsl(random()))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "id": "ckjOCzI3np1A" }, "source": [ "## Inset, offset, take, divide, subdivide...\n", "\n", "All `@renderables` have a rectangle associated with them (the full rectangle of the artifact canvas), and all rendering functions are passed rectangles, either via the first and only argument, or as a property of the first argument, as is the case with @animation renderables, which pass a Frame argument that makes the rectangle accessible via f.a.r (where f is the Frame).\n", "\n", "But we’re getting ahead of ourselves.\n", "\n", "A Rect has lots of methods, though the most useful ones are `inset`, `offset`, `take`, `divide`, and `subdivide`.\n", "\n", "Here’s a simple example that insets, offsets, subtracts, and then subtracts again. (Probably not something I’d write in reality, but good for demonstration purposes.)" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 171 }, "id": "CH7Y341Ffr_M", "outputId": "7e634816-72a2-4680-832c-e8c966c32a54" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((700, 300))\n", "def r2(r):\n", " r1 = (r.take(0.5, \"W\") # \"W\" for \"West\"\n", " .inset(20, 20)\n", " .offset(0, 10)\n", " .subtract(20, \"E\")\n", " .subtract(10, \"N\"))\n", " \n", " return (P().rect(r1)\n", " .f(hsl(0.5)))" ] }, { "cell_type": "markdown", "metadata": { "id": "-OMT57J2n5UW" }, "source": [ "## More complex slicing & dicing\n", "\n", "You may have noticed that the rect functions take a mix of float and int arguments. That’s because a value less than `1.0` will be treated, by the dividing-series of rect functions, as percentages of the dimension implied by the edge argument. So in that `take(0.5, \"W\")` above, the `0.5` specifies 50% of the width of the rectangle (width because of the `W` edge argument).\n", "\n", "Here’s an example that divides a rectangle into left and right rectangles, and shows another useful method, `square` (which takes the largest square possible from the center of the given rectangle)." ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 171 }, "id": "40sYBsjRmdL4", "outputId": "3c23d29d-e029-4720-f17a-c893a2758af7" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((700, 300))\n", "def lr(r):\n", " ri = r.inset(50, 50)\n", " left, right = ri.divide(0.5, \"W\")\n", " return P(\n", " P().rect(ri).fssw(-1, 0.75, 2),\n", " P().oval(left\n", " .square()\n", " .offset(100, 0))\n", " .f(hsl(0.6, a=0.5)),\n", " P().oval(right\n", " .square()\n", " .inset(-50))\n", " .f(hsl(0, a=0.5)))" ] }, { "cell_type": "markdown", "metadata": { "id": "Hh48BjajoGFA" }, "source": [ "Here’s an example using subdivide to subdivide a larger rectangle into smaller pieces, essentially columns." ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 171 }, "id": "qtieb1K9mzhc", "outputId": "f58b84b5-b73d-492e-c3a8-742597f31c89" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((700, 300))\n", "def columns(r):\n", " cs = r.inset(10).subdivide(5, \"W\")\n", " return P.Enumerate(cs, lambda x:\n", " P(x.el.inset(10)).f(hsl(random())))" ] }, { "cell_type": "markdown", "metadata": { "id": "Kk_wwlkdoITO" }, "source": [ "Of course, columns like that aren’t very typographic. Here’s an example using subdivide_with_leading, a useful method for quickly getting standard rows or columns with classic spacing.\n", "\n" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 271 }, "id": "tGwwRSXUm3hW", "outputId": "4350d3b4-1cf0-47a2-ab44-e29c5994d027" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((700, 500))\n", "def columns_leading(r):\n", " cs = r.subdivide_with_leading(5, 20, \"N\")\n", " return P.Enumerate(cs, lambda x:\n", " P(x.el).f(hsl(random())))" ] }, { "cell_type": "markdown", "metadata": { "id": "Wrd5-yqNScij" }, "source": [ "## Grids\n", "\n", "If you’re a fan of CSS grids, you might like the weird little `Grid` class made available in Coldtype." ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 171 }, "id": "MsEr0gVSSj7s", "outputId": "b700bffe-5945-4c9e-8bca-825a35a3b903" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "r = Rect(700, 300)\n", "g = Grid(r, \"a a\", \"a a\", \"a b / c d\")\n", "\n", "@renderable(r)\n", "def simple_grid1(r):\n", " return P(\n", " P(g[\"a\"]).f(hsl(0)),\n", " P(g[\"d\"]).f(hsl(0.5)))" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 171 }, "id": "fWfZ4Hf4TSEt", "outputId": "7c764616-c699-4858-eac0-6d2c3120b087" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "g = Grid(r, \"a 100\", \"100 a\", \"a b / c d\")\n", "\n", "@renderable(r)\n", "def simple_grid2(r):\n", " return (\n", " P(g[\"a\"]).f(hsl(0)),\n", " P(g[\"d\"]).f(hsl(0.5)))" ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 171 }, "id": "MsbQ7JoxTWiP", "outputId": "25f554ac-e87a-4e11-a75d-c4942c923144" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "g = Grid(r, \"a 100 a\", \"a 100 a\", \"a b c / d e f / g h i\")\n", "\n", "@renderable(r)\n", "def simple_grid3(r):\n", " return (\n", " P(g[\"a\"]).f(hsl(0)),\n", " P(g[\"e\"]).f(hsl(0.3)),\n", " P(g[\"i\"]).f(hsl(0.5)))" ] } ], "metadata": { "colab": { "collapsed_sections": [], "name": "Coldtype Geometry Tutorial.ipynb", "provenance": [] }, "kernelspec": { "display_name": "Python 3.10.5 ('venv': venv)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.5" }, "vscode": { "interpreter": { "hash": "10cf79b6252b6bfa5f219a04587890ec267e7f2fde6b173960de4ad2915a3b2e" } } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: examples/sites/coldtype.goodhertz.com/pages/tutorials/shapes.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dict(\n", " author=\"Rob Stenson\",\n", " title=\"Shapes\",\n", " date=\"11/29/2022\"\n", ")" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "id": "joXt9bJEjUDC" }, "outputs": [], "source": [ "#hide-publish\n", "%pip install -q coldtype[notebook]\n", "#%pip install -q \"coldtype[notebook] @ git+https://github.com/goodhertz/coldtype\"\n", "from coldtype.notebook import *" ] }, { "cell_type": "markdown", "metadata": { "id": "cqE4kXaXoiFu" }, "source": [ "Let’s start with a classic rectangle." ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 171 }, "id": "-UPVdBQqjeYF", "outputId": "3b300b98-d826-455e-f10c-5f4272984935" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((300, 300))\n", "def rectangle(r):\n", " return P().rect(r.inset(50)).f(hsl(0.9, 0.75))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "id": "JmyZZoKRoshG" }, "source": [ "That’s how to draw a rectangle with a 50px padding around the edges (the padding comes from the ``r.inset(50)`` call).\n", "\n", "_I’m re-reading this now and if you’re thinking to yourself: that’s a square — well that makes sense, but a square’s just a rectangle with the same width & height._\n", "\n", "How about an oval?" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 171 }, "id": "14h2ExQojjr2", "outputId": "cdfd9fd5-f304-4912-853c-b9800bece808" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((300, 300))\n", "def oval(r):\n", " return P().oval(r.inset(45)).f(hsl(0.6))" ] }, { "cell_type": "markdown", "metadata": { "id": "TCyBoKKXoutt" }, "source": [ "That’s an oval. Sweeet.\n", "\n", "What if you want to combine an oval and a rect?" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 171 }, "id": "2Ae7AcCBjsJa", "outputId": "11eee8a0-e820-4ff6-c246-8fd61595f4ff" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((300, 300))\n", "def ovalrect(r):\n", " return (P()\n", " .oval(r.inset(60))\n", " .translate(30, 30)\n", " .union(P()\n", " .rect(r.inset(65))\n", " .translate(-30, -30))\n", " .f(hsl(0.05, l=0.6, s=0.75)))" ] }, { "cell_type": "markdown", "metadata": { "id": "rFNQ6Mqxp8dT" }, "source": [ "Or maybe you want just the parts of those two shapes that don’t overlap? And maybe you want to fill the shape with a gradient and rotate the rect a little bit and then eyeball how it should be centered in its frame?" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 171 }, "id": "j4EPWL3Sp2KF", "outputId": "280fa279-b2e4-4841-8d18-383e312ac629" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((300, 300))\n", "def ovalrect_diff(r):\n", " return (P()\n", " .oval(r.inset(60))\n", " .translate(30, 30)\n", " .xor(P()\n", " .rect(r.inset(65))\n", " .translate(-30, -30)\n", " .rotate(-5))\n", " .f(Gradient.Horizontal(r,\n", " hsl(0.05, l=0.6, s=0.75),\n", " hsl(0.8, l=0.6, s=0.5)))\n", " .translate(7)) # & eyeball it" ] }, { "cell_type": "markdown", "metadata": { "id": "iEgF3YhNa3ko" }, "source": [ "## Modifying Shapes\n", "\n", "Here’s an example of building up a chain of effects to modify a simple vector shape." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 171 }, "id": "SRiiWR6ZZ1O4", "outputId": "6182fe47-84f7-4276-fe52-e31fa9b7ce10" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from coldtype.fx.skia import phototype\n", "\n", "@renderable((300, 300))\n", "def ovalmod(r):\n", " return (P()\n", " .oval(r.inset(60))\n", " .flatten(5) # breaks the oval down into non-curves, 5 is the length of the segment\n", " .roughen(50) # randomizes the vertices of the shape\n", " .f(1)\n", " .ch(phototype(r, blur=5, cut=100, cutw=5, fill=hsl(0.07, 1, 0.6))))" ] } ], "metadata": { "colab": { "collapsed_sections": [], "provenance": [] }, "kernelspec": { "display_name": "Python 3.10.5 ('venv': venv)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.5" }, "vscode": { "interpreter": { "hash": "10cf79b6252b6bfa5f219a04587890ec267e7f2fde6b173960de4ad2915a3b2e" } } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: examples/sites/coldtype.goodhertz.com/pages/tutorials/text.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dict(\n", " author=\"Rob Stenson\",\n", " title=\"Text\",\n", " date=\"11/29/2022\"\n", ")" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "id": "joXt9bJEjUDC" }, "outputs": [], "source": [ "#hide-publish\n", "%pip install -q \"coldtype[notebook]\"\n", "#!pip install -q \"coldtype[notebook] @ git+https://github.com/goodhertz/coldtype\"\n", "\n", "from coldtype.notebook import *" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "A few common variables we’ll be using throughout:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "r = Rect(1000, 250)\n", "co = Font.ColdtypeObviously()" ] }, { "cell_type": "markdown", "metadata": { "id": "cqE4kXaXoiFu" }, "source": [ "## Basic Text\n", "\n", "Let’s start with a simple \"Hello World\", except in this case, let’s just say COLDTYPE, because the coldtype repository has a special version of the font Obviously that just has those letters (CDELOPTY)." ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 122 }, "id": "-UPVdBQqjeYF", "outputId": "8cc34e53-1dcd-4c79-da7f-776b812da033" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((800, 150))\n", "def text1(r):\n", " return StSt(\"COLDTYPE\", co, 150).align(r)" ] }, { "cell_type": "markdown", "metadata": { "id": "JmyZZoKRoshG" }, "source": [ "You might be wondering why the text is blue — that’s the default fill color for any text in coldtype. Let’s mess with the color and set some variable font axis values:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 126 }, "id": "14h2ExQojjr2", "outputId": "4b962499-ac46-42f3-e7a3-1a448f15a960" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((800, 150))\n", "def text2(r):\n", " return (StSt(\"CDELOPTY\", co, 150, wdth=0.5)\n", " .f(hsl(0.8, s=0.75))\n", " .align(r))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Also let’s track out the letters and rotate them." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((800, 150))\n", "def text3(r):\n", " return (StSt(\"CDELOPTY\", co, 150\n", " , wdth=0.5\n", " , rotate=10\n", " , tu=150)\n", " .f(hsl(0.9, s=0.75))\n", " .align(r))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "id": "TCyBoKKXoutt" }, "source": [ "What’s interesting (and different) about setting text with Coldtype is that you aren’t telling the computer to draw text, you’re asking for information about the individual glyphs and where they sit, given the parameters you’re passing into `StSt` function.\n", "\n", "Put another way, what you get back from calling (`StSt`...) is a rich set of data that can be inspected and manipulated." ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 129 }, "id": "2Ae7AcCBjsJa", "outputId": "ef4d8b3e-a2b1-4404-b19a-dde422bc906f" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((800, 150))\n", "def text4(r):\n", " pens = (StSt(\"COLDTYPE\", co, 150\n", " , wdth=0.5\n", " , rotate=10\n", " , tu=150)\n", " .f(Gradient.Vertical(r,\n", " hsl(0.5, s=0.8),\n", " hsl(0.8, s=0.75)))\n", " .align(r))\n", "\n", " pens[0].rotate(180)\n", " pens[-1].rotate(180)\n", " pens[-2].rotate(10)\n", " return pens" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "j4EPWL3Sp2KF", "outputId": "6c8b8b06-495b-458d-8141-934c0928c6b8" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", " <®:P:/4...>\n", " - <®:P:RecordingPen(12mvs) {frame=Rect(205.08,18.75,93.45,112.50),glyphName=C}>\n", " - <®:P:RecordingPen(12mvs) {frame=Rect(298.53,18.75,110.70,112.50),glyphName=O}>\n", " - <®:P:RecordingPen(12mvs) {frame=Rect(409.23,18.75,70.65,112.50),glyphName=L}>\n", " - <®:P:RecordingPen(18mvs) {frame=Rect(479.88,18.75,108.15,112.50),glyphName=D}>\n" ] }, { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((800, 150))\n", "def text_print(r):\n", " pens = (StSt(\"COLD\", co, 150).align(r))\n", " print(pens.tree()) # <<< printing here\n", " return pens" ] }, { "cell_type": "markdown", "metadata": { "id": "iEgF3YhNa3ko" }, "source": [ "### _An aside_\n", "\n", "Just for fun, here’s a more coldtype-y way to do that last manipulation: rather than declare a variable `pens` and then manipulate it in a line-wise fashion (`pens[0].rotate...`), you can use functions built-in to the `P` class to manipulate the form without leaving the \"chain\" (that is, the chained expression).\n", "\n", "So the keys here are the lines `.indices` and `.index`, which target elements of the tree (using standard python-style indexing), and then give you the opportunity to do anything you'd like in a lambda. So in just one line, we can rotate both the `C` (index 0) and the `E` (index -1) with a single lambda (i.e. a function that, in this case, is called for each index).\n", "\n", "Also we can change the colors since who wants to see the same colors again." ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 129 }, "id": "SRiiWR6ZZ1O4", "outputId": "189ee78d-1ba0-41e2-94aa-c6a8a87997ca" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((800, 150))\n", "def text4_more_coldtypey(r):\n", " return (StSt(\"COLDTYPE\", co, 150\n", " , wdth=0.5\n", " , rotate=10\n", " , tu=150)\n", " .align(r)\n", " .indices([0, -1], lambda p: p.rotate(180))\n", " .index(-2, lambda p: p.rotate(10))\n", " .ch(lambda p: p\n", " .f(Gradient.V(p.ambit(),\n", " hsl(0.55, 1),\n", " hsl(0.45, 1)))))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "id": "rFNQ6Mqxp8dT" }, "source": [ "### Debugging\n", "\n", "To see a tree-like representation of the vector/text data, try something like this:" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "id": "_qxJQCJCqE-N" }, "source": [ "## Less Basic Text\n", "\n", "Usually, glyph-wise structured representation of text is not a feature of software or software libraries, because when programmers sit down to implement support for text, they do it with the understanding that if you want text, you probably want a _lot_ of text — great big slabs of it, set in large blocks, like this paragraph that you’re reading now.\n", "\n", "But for lots of graphic design (particularly animation), what you actually want is very precise control over only a few glyphs, maybe a line or two. That was some of the magic of technologies like moveable type, or especially Letraset; they gave designers direct control over letterforms. A lot like when you hit “Convert to Outlines” in Illustrator today.\n", "\n", "Of course, there’s a big downside to converting-to-outlines: it is excruciatingly slow. And more than that, even when you’re working with just a few letters, you might need to change those letters at the last minute, right before a project is due.\n", "\n", "Which is where code really shines. All the manipulations I’ve done so far are not “one-way” or “destructive” (like Convert to Outlines). As far as we’re concerned, the “textbox” (so to speak) is still intact: `StSt(\"COLDTYPE\"...`" ] }, { "cell_type": "markdown", "metadata": { "id": "TqWObH10qPVk" }, "source": [ "To illustrate that point, let’s change the text:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 127 }, "id": "Nip4LuANj8PQ", "outputId": "3ef035a8-acc6-4a4c-facb-ee0c109e3492" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((800, 150))\n", "def text_rotations(r):\n", " pens = (StSt(\"TYPECOLD\", co, 150,\n", " wdth=0.5, rotate=10, tu=150)\n", " .f(0)\n", " .align(r))\n", "\n", " pens[0].rotate(180)\n", " pens[-1].rotate(180)\n", " pens[-2].rotate(10)\n", " return pens" ] }, { "cell_type": "markdown", "metadata": { "id": "OQkREYLnqTig" }, "source": [ "The last two examples also illustrate something important about Coldtype: (almost) everything is mutable/self-changing by default. So a line like `pens[0].rotate(180)` changes `pens[0]` directly, meaning you don’t need to assign it to a new variable. This makes it very easy to directly manipulate nested structures without needing to reassign variables.\n", "\n", "This also means that sometimes it is very necessary to copy pens in order to double them, so you can make changes to a copy without modifying the original. \n", "\n", "For instance:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 131 }, "id": "Pa54w9yzkBRS", "outputId": "3c09e4cf-0a2b-44df-fab8-4107be3816ae" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((800, 200))\n", "def text_outline1(r):\n", " pens = (StSt(\"TYPECOLD\", co, 150, wdth=0.5, rotate=10, tu=250)\n", " .align(r))\n", "\n", " return P(\n", " pens.copy().translate(10, -10).f(0),\n", " pens.f(1).s(hsl(0.9)).sw(3))" ] }, { "cell_type": "markdown", "metadata": { "id": "FEVZgldhqf0v" }, "source": [ "I’ll admit the impact of the interesting dropshadow here is lessened somewhat by the appearance of the strange pink lines in the top layer of text. When I added the code stroking the pens `(.s(hsl(0.9)).sw(3))`, I thought it would look like a standard stroked shape. But if you’re familiar with how variable fonts are constructed, those lines might not seem all that strange to you — they indicate that the letters are constructed in order to interpolate cleanly. That said, we probably don’t want to see them!\n", "\n", "How to get rid of them? There are a couple options:\n", "\n", "- There’s a special `ro=1` flag that you can pass to any `StSt` function, and that’ll (`r`)emove (`o`)verlaps on all the glyphs before they come back to you in their correct positions." ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 164 }, "id": "8r4WIN_rkGN1", "outputId": "87ffa9bd-7110-458a-bf55-3ec7c1a14205" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((800, 200))\n", "def text_outline2(r):\n", " return (StSt(\"TYPECOLD\", co, 150, wdth=0.5, rotate=10, tu=100, ro=1)\n", " .pen()\n", " .layer(\n", " lambda p: p.castshadow(-45, 50).f(0),\n", " lambda p: p.fssw(1, hsl(0.9), 3))\n", " .align(r, ty=1)) # ty=1 to use new contours of shadows for aligning (ty means \"true-y\", i.e. ignore the font metrics)" ] }, { "cell_type": "markdown", "metadata": { "id": "_-G0TNx0AuRL" }, "source": [ "- Or you could call `removeOverlap` after you do the lockup, which would have the same effect." ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 164 }, "id": "Ii7Xpp72As6O", "outputId": "717b9671-75ea-47a2-9be3-ff7bd41899ee" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((800, 200))\n", "def text_outline3(r):\n", " return (StSt(\"TYPECOLD\", co, 150, wdth=0.5, rotate=10, tu=100)\n", " .pen()\n", " .removeOverlap()\n", " .layer(\n", " lambda p: p.castshadow(-45, 50).f(0),\n", " lambda p: p.fssw(1, hsl(0.9), 3))\n", " .align(r, ty=1))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "id": "VqEw2vdIqxX_" }, "source": [ "Fixed! Also I did some completely unrelated things there.\n", "\n", "- Instead of simply offsetting the main text to get a shadow, this example collapses the set of pens to a single pen (via `.pen()`), and then uses a built-in method called `castshadow(, )` to cast a shadow.\n", "\n", "- Instead of copying anything, the example now uses the `.layer` method, which does the copying for you, and also removes the “original” in favor of the two lambda functions provided. Basically, this is a copy-and-replace operation, where we’ve provided two “replace” operations. So to recap: via `StSt`, we created a set of pens (aka vectors aka shapes), then we reduced that to a single pen, then we layered that pen, resulting in two new pens (one for the shadow, one for the filled and stroked shape on top). The layer function also allows us to directly return the “chain,” without having to declare intermediate variables." ] }, { "cell_type": "markdown", "metadata": { "id": "olPrASFArG5C" }, "source": [ "One additional refinement you may want to make in an example like this is that you’d want to individually cast shadows based on a glyph + a little bit of stroke set around it, in the style of the 19th-century type designers. So let’s do that:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 157 }, "id": "8IflJsAqkXEP", "outputId": "d9cf32e2-0c9c-4e9e-cf24-49285462012a" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((800, 250))\n", "def text_outline4(r):\n", " return (StSt(\"COLDTYPE\", co, 150\n", " , wdth=0.5\n", " , rotate=10\n", " , tu=100\n", " , ro=1)\n", " .f(1)\n", " .layer(\n", " lambda ps: ps.mapv(lambda p: p\n", " .outline(10)\n", " .removeOverlap()\n", " .castshadow(-45, 50)\n", " .f(None)\n", " .s(hsl(0.6, s=1, l=0.4))\n", " .sw(4)),\n", " lambda ps: ps.s(hsl(0.9)).sw(4))\n", " .align(r, ty=1))" ] }, { "cell_type": "markdown", "metadata": { "id": "iOZSb4kcrRWh" }, "source": [ "Dang, you know I thought that example would just work, but it looks like there are some tiny little dots present, which I think are artifacts of the `castshadow` call. I didn’t write the guts of that (Loïc Sander wrote something called a TranslationPen which is used by coldtype internally), so I don’t understand it completely, but it shouldn’t be difficult to devise a way to clean up those tiny specks by testing the `bounds` of each of the contours created by the `TranslationPen`. We can do that by iterating over the individual contours by `explode`ing the path into its constituent contours, then `filter`ing those contours, these `implode`ing those contours back into a single path again. We can also use the opportunity demonstrate some debugging techniques, like isolating a single letter and blowing it up." ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 294 }, "id": "i9mSNg4ykl9m", "outputId": "8239dd3d-4a84-4ed3-eab4-e8a6cfcbbd6c" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "def shadow_and_clean(p):\n", " return (p\n", " .outline(10)\n", " .reverse()\n", " .remove_overlap()\n", " .castshadow(-5, 500)\n", " .explode()\n", " .filter(lambda j, c: c.bounds().w > 50)\n", " .implode()\n", " .f(None)\n", " .s(hsl(0.6, s=1, l=0.4))\n", " .sw(4))\n", "\n", "@renderable((800, 500))\n", "def text_outline5(r):\n", " return (StSt(\"O\", co, 500\n", " , wdth=0.5, rotate=10, tu=100, ro=1)\n", " .f(1)\n", " .layer(\n", " lambda ps: ps.mapv(shadow_and_clean),\n", " lambda ps: ps.s(hsl(0.9)).sw(4))\n", " .align(r, ty=1))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "id": "yuTeXQOGrd18" }, "source": [ "Got it! If you comment out the `.explode/.filter/.implode` lines, you should see the little speck show up again.\n", "\n", "N.B. We pulled the lambda being passed to mapv (map-values) out into its own function, `shadow_and_clean`. It’s not really a “reusable” function, but it is a little clearer in this instance to have that logic separated from the main chained expression." ] }, { "cell_type": "markdown", "metadata": { "id": "9cB9hIhTrftC" }, "source": [ "Two suggestions to help you better understand code or find weird looks: try commenting out various stuff and using random colors." ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 157 }, "id": "272Qo4Lxku-1", "outputId": "4a90976f-7bed-46e1-8bbc-c2cf6ba3eedb" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((800, 250))\n", "def text_outline_random(r):\n", " return (StSt(\"COLDTYPE\", co, 150\n", " , wdth=0.5, rotate=10, tu=100, ro=1)\n", " .f(1)\n", " .layer(\n", " lambda ps: ps.mapv(lambda p: p\n", " .outline(10)\n", " #.remove_overlap()\n", " .castshadow(-45, 50)\n", " .f(hsl(random(), s=1, a=0.1))\n", " .s(hsl(random(), s=1, l=0.4))\n", " .sw(4)),\n", " lambda ps: ps.mapv(lambda p: p\n", " .s(hsl(random())).sw(4)))\n", " .align(r, ty=1))" ] }, { "cell_type": "markdown", "metadata": { "id": "O73qZ6Nxrjwu" }, "source": [ "# Typography, further afield\n", "\n", "## Multi-line text" ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 297 }, "id": "Nerhb3Rud_03", "outputId": "c14a1647-a452-4f16-dee0-c92253ff1bdc" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "<®:P:/2...>" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "(StSt(\"COLDTYPE\\nTYPECOLD\", co, 200, wdth=1)\n", " .f(0))" ] }, { "cell_type": "markdown", "metadata": { "id": "QJNEoRSSeCl3" }, "source": [ "## Multi-line text, fitted to width\n", "\n", "Via the `fit=` kwarg" ] }, { "cell_type": "code", "execution_count": 32, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 296 }, "id": "KGv9v_OHk3AQ", "outputId": "84472b14-6f09-4f14-8969-281b8af06e3a" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "<®:P:/2...>" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "(StSt(\"COLDTYPE\\nTYPECOLD\", co, 200\n", " , wdth=1\n", " , fit=500)\n", " .f(0))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "id": "RD0F6CbXeOJR" }, "source": [ "## Multi-line text, aligned right" ] }, { "cell_type": "code", "execution_count": 33, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 289 }, "id": "mHMKr5h4t32w", "outputId": "81f4eaa6-e9a9-48cf-8825-9773904c77a1" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "<®:P:/2...>" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "r1 = Rect(1000, 500)\n", "\n", "(StSt(\"LEDC\\nCOLDTYPE\", co, 200, wdth=0)\n", " .xalign(r1, \"E\")\n", " .align(r1)\n", " .f(0))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Load Fonts directly from Google\n", "\n", "Here’s an example of loading a font from Google, using `Font.GoogleFont`:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "text1 = \"\"\"\n", "از آنجا که عدم شناسائی و تحقیر حقوق\n", "بشر منتهی به اعمال وحشیانه‌ای\n", "\"\"\"\n", "\n", "@renderable((1080, 340), bg=1)\n", "def arefRuqaa(r):\n", " return (StSt(text1, Font.GoogleFont(\"Aref Ruqaa Ink\"), 80)\n", " .xalign(r, \"E\")\n", " .lead(60)\n", " .align(r))" ] }, { "cell_type": "markdown", "metadata": { "id": "uW3-Ajgprw7N" }, "source": [ "## Text-on-a-path\n", "\n", "If you’d like to align glyphs along an arbitrary path, you can use the DATPens’ `distribute_on_path` method to set the glyphs returned from a StSt." ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 375 }, "id": "PaD3GTSFk9Xt", "outputId": "76f0d75e-daf4-46aa-f8d4-ece3dacd8401" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((540, 540))\n", "def text_on_path1(r):\n", " circle = P().oval(r.inset(130)).reverse()\n", "\n", " txt = (StSt(\"COLDTYPE\", co, 100, wdth=1)\n", " .distribute_on_path(circle, offset=180)\n", " .f(0))\n", "\n", " return (P(\n", " circle.scale(0.9).fssw(-1, 0, 3),\n", " txt)\n", " .align(r))" ] }, { "cell_type": "markdown", "metadata": { "id": "hUV0ruHsr3GV" }, "source": [ "What if we want more text on the circle and we want it to fit automatically to the length of the curve on which it’s set — without overlapping? Simple append a fit= keyword argument to fit the text to the length of the curve that we’ll end up setting the pens on." ] }, { "cell_type": "code", "execution_count": 63, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 514 }, "id": "AfHtWo-nm1Ma", "outputId": "40e646f2-f7c4-447b-c183-1d46ca882786" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((540, 540))\n", "def text_on_path2(r):\n", " circle = P().oval(r.inset(50)).reverse()\n", "\n", " return (StSt(\"COLDTYPE COLDTYPE COLDTYPE \", co, 100\n", " , wdth=1\n", " , tu=100\n", " , space=500\n", " , strip=False\n", " , fit=circle.length())\n", " .distribute_on_path(circle)\n", " .f(Gradient.H(circle.bounds(),\n", " hsl(0.5, s=0.6),\n", " hsl(0.85, s=0.6)))\n", " .insert(0,\n", " P().oval(r.inset(70)).fssw(-1, 0.75, 3))\n", " .scale(0.75, point=r.pc))" ] }, { "cell_type": "markdown", "metadata": { "id": "UW0Ia_JXr5Zo" }, "source": [ "One thing that’s weird about setting text on a curve is that, depending on the curve, it can exaggerate — or eliminate — spacing between letters. Sometimes that doesn’t really matter — in the case of this circle, because the curve only bends in one manner, the text is always extra spacey, which usually isn’t a problem. But if we set the text on a sine-wave, the issue becomes more apparent, since the spacing is both expanded and compressed on the same curve, and when letters overlap excessively, they can get illegible quickly.\n", "\n", "Is there a solution? Probably many but the one I like a lot is the understroke method on the DATPens class, which interleaves a stroked version of each letter in a set (a technique popular in pulp/comic titling & the subsequent graffiti styles they inspired)." ] }, { "cell_type": "markdown", "metadata": { "id": "3v5klt4Er8Y4" }, "source": [ "Let’s see what that looks like." ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 314 }, "id": "jG9FcCaPnMZP", "outputId": "b742e589-27e9-40e8-b5a5-7b978c487702" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import coldtype.fx.shapes as shapes\n", "\n", "@renderable((1000, 500), bg=0.95)\n", "def sine1(r):\n", " sine = P().ch(shapes.sine(r.inset(0, 180), 3))\n", "\n", " return (StSt(\"COLDTYPE COLDTYPE COLDTYPE\", co, 100\n", " , wdth=1\n", " , tu=-50\n", " , space=500\n", " , fit=sine.length()\n", " )\n", " .distribute_on_path(sine)\n", " .understroke(sw=10)\n", " .f(Gradient.H(sine.bounds(),\n", " hsl(0.7, l=0.6, s=0.65),\n", " hsl(0.05, l=0.6, s=0.65)))\n", " .translate(0, -20))" ] }, { "cell_type": "markdown", "metadata": { "id": "gBy1BR-or-Ql" }, "source": [ "Interesting! But there’s one thing to correct if we want better legibility. You’ll notice in that first purple COLDTYPE, the C is unrecognizable, because the O that comes after it is on top of it. This is how text layout engines usually work for LTR languages — the topmost glyph is the right-most glyph. But that’s not what we want — we want to reverse the order of the glyphs. Luckily, that’s easy, just pass a r=1 (or reverse=1), to the Style constructor." ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 314 }, "id": "Q1ioT737nZR_", "outputId": "3610e20a-7913-4a55-fe4d-e905a83858f6" }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "@renderable((1000, 500), bg=0.95)\n", "def sine2(r):\n", " sine = P().ch(shapes.sine(r.inset(0, 180), 3))\n", "\n", " return (StSt(\"COLDTYPE COLDTYPE COLDTYPE\", co, 100\n", " , wdth=1\n", " , tu=-50\n", " , space=500\n", " , r=1\n", " , fit=sine.length()\n", " )\n", " .distribute_on_path(sine)\n", " .understroke(sw=10)\n", " .f(Gradient.H(sine.bounds(),\n", " hsl(0.7, l=0.7, s=0.65),\n", " hsl(0.05, l=0.6, s=0.65)))\n", " .translate(0, -20))" ] }, { "cell_type": "markdown", "metadata": { "id": "L1xL5OA5r_6k" }, "source": [ "It’s a subtle change, but one that (to me) makes a huge difference. I also lightened the purple in the gradient, I think it looks a little better that way, right?\n", "\n" ] } ], "metadata": { "colab": { "collapsed_sections": [], "name": "Coldtype Text Tutorial.ipynb", "provenance": [] }, "kernelspec": { "display_name": "Python 3.10.5 ('venv': venv)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.5" }, "vscode": { "interpreter": { "hash": "10cf79b6252b6bfa5f219a04587890ec267e7f2fde6b173960de4ad2915a3b2e" } } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: examples/sites/coldtype.goodhertz.com/templates/_docs.j2 ================================================
{% for k,v in docs.items() %}

{{k}}

    {% for d in v %}
  • {{ d.itself }}

    {{ d.signature }}
    {{ d.docstring }}
    {% if d.methods %}
      {% for method in d.methods %}
    • {{ method.itself.__name__ }}

      {{ method.signature }}
      {{ method.docstring }}
    • {% endfor %}
    {% endif %}
  • {% endfor %}
{% endfor %}
================================================ FILE: examples/sites/coldtype.goodhertz.com/templates/_footer.j2 ================================================ ================================================ FILE: examples/sites/coldtype.goodhertz.com/templates/_header.j2 ================================================ {% if url == "/" %}

Coldtype Tutorials

{% else %}

Coldtype Tutorials

{% endif %} {{ nav_html|safe }} ================================================ FILE: examples/sites/coldtype.goodhertz.com/templates/_page.j2 ================================================

{{ page.title }}

{% if page.data["notebook"] %} {% endif %}
{{ page.content|safe }}
================================================ FILE: examples/sites/coldtype.goodhertz.com/templates/index.j2 ================================================ ================================================ FILE: examples/sites/coldtype.goodhertz.com/templates/partials/sidebar.j2 ================================================ ================================================ FILE: examples/sites/coldtype.p5js/assets/hbjs.js ================================================ function hbjs(instance) { 'use strict'; var exports = instance.exports; var heapu8 = new Uint8Array(exports.memory.buffer); var heapu32 = new Uint32Array(exports.memory.buffer); var heapi32 = new Int32Array(exports.memory.buffer); var heapf32 = new Float32Array(exports.memory.buffer); var utf8Decoder = new TextDecoder("utf8"); var HB_MEMORY_MODE_WRITABLE = 2; function hb_tag(s) { return ( (s.charCodeAt(0) & 0xFF) << 24 | (s.charCodeAt(1) & 0xFF) << 16 | (s.charCodeAt(2) & 0xFF) << 8 | (s.charCodeAt(3) & 0xFF) << 0 ); } function _hb_untag(tag) { return [ String.fromCharCode((tag >> 24) & 0xFF), String.fromCharCode((tag >> 16) & 0xFF), String.fromCharCode((tag >> 8) & 0xFF), String.fromCharCode((tag >> 0) & 0xFF) ].join(''); } function _buffer_flag(s) { if (s == "BOT") { return 0x1; } if (s == "EOT") { return 0x2; } if (s == "PRESERVE_DEFAULT_IGNORABLES") { return 0x4; } if (s == "REMOVE_DEFAULT_IGNORABLES") { return 0x8; } if (s == "DO_NOT_INSERT_DOTTED_CIRCLE") { return 0x10; } return 0x0; } /** * Create an object representing a Harfbuzz blob. * @param {string} blob A blob of binary data (usually the contents of a font file). **/ function createBlob(blob) { var blobPtr = exports.malloc(blob.byteLength); heapu8.set(new Uint8Array(blob), blobPtr); var ptr = exports.hb_blob_create(blobPtr, blob.byteLength, HB_MEMORY_MODE_WRITABLE, blobPtr, exports.free_ptr()); return { ptr: ptr, /** * Free the object. */ destroy: function () { exports.hb_blob_destroy(ptr); } }; } /** * Create an object representing a Harfbuzz face. * @param {object} blob An object returned from `createBlob`. * @param {number} index The index of the font in the blob. (0 for most files, * or a 0-indexed font number if the `blob` came form a TTC/OTC file.) **/ function createFace(blob, index) { var ptr = exports.hb_face_create(blob.ptr, index); const upem = exports.hb_face_get_upem(ptr); return { ptr: ptr, upem, /** * Return the binary contents of an OpenType table. * @param {string} table Table name */ reference_table: function(table) { var blob = exports.hb_face_reference_table(ptr, hb_tag(table)); var length = exports.hb_blob_get_length(blob); if (!length) { return; } var blobptr = exports.hb_blob_get_data(blob, null); var table_string = heapu8.subarray(blobptr, blobptr+length); return table_string; }, /** * Return variation axis infos */ getAxisInfos: function() { var axis = exports.malloc(64 * 32); var c = exports.malloc(4); heapu32[c / 4] = 64; exports.hb_ot_var_get_axis_infos(ptr, 0, c, axis); var result = {}; Array.from({ length: heapu32[c / 4] }).forEach(function (_, i) { result[_hb_untag(heapu32[axis / 4 + i * 8 + 1])] = { min: heapf32[axis / 4 + i * 8 + 4], default: heapf32[axis / 4 + i * 8 + 5], max: heapf32[axis / 4 + i * 8 + 6] }; }); exports.free(c); exports.free(axis); return result; }, /** * Free the object. */ destroy: function () { exports.hb_face_destroy(ptr); }, }; } var pathBufferSize = 65536; // should be enough for most glyphs var pathBuffer = exports.malloc(pathBufferSize); // permanently allocated /** * Create an object representing a Harfbuzz font. * @param {object} blob An object returned from `createFace`. **/ function createFont(face) { var ptr = exports.hb_font_create(face.ptr); /** * Return a glyph as an SVG path string. * @param {number} glyphId ID of the requested glyph in the font. **/ function glyphToPath(glyphId) { var svgLength = exports.hbjs_glyph_svg(ptr, glyphId, pathBuffer, pathBufferSize); return svgLength > 0 ? utf8Decoder.decode(heapu8.subarray(pathBuffer, pathBuffer + svgLength)) : ""; } return { ptr: ptr, glyphToPath: glyphToPath, /** * Return a glyph as a JSON path string * based on format described on https://svgwg.org/specs/paths/#InterfaceSVGPathSegment * @param {number} glyphId ID of the requested glyph in the font. **/ glyphToJson: function (glyphId) { var path = glyphToPath(glyphId); return path.replace(/([MLQCZ])/g, '|$1 ').split('|').filter(function (x) { return x.length; }).map(function (x) { var row = x.split(/[ ,]/g); return { type: row[0], values: row.slice(1).filter(function (x) { return x.length; }).map(function (x) { return +x; }) }; }); }, /** * Set the font's scale factor, affecting the position values returned from * shaping. * @param {number} xScale Units to scale in the X dimension. * @param {number} yScale Units to scale in the Y dimension. **/ setScale: function (xScale, yScale) { exports.hb_font_set_scale(ptr, xScale, yScale); }, /** * Set the font's variations. * @param {object} variations Dictionary of variations to set **/ setVariations: function (variations) { var entries = Object.entries(variations); var vars = exports.malloc(8 * entries.length); entries.forEach(function (entry, i) { heapu32[vars / 4 + i * 2 + 0] = hb_tag(entry[0]); heapf32[vars / 4 + i * 2 + 1] = entry[1]; }); exports.hb_font_set_variations(ptr, vars, entries.length); exports.free(vars); }, /** * Free the object. */ destroy: function () { exports.hb_font_destroy(ptr); } }; } var utf8Encoder = new TextEncoder("utf8"); function createCString(text) { var bytes = utf8Encoder.encode(text); var ptr = exports.malloc(bytes.byteLength); heapu8.set(bytes, ptr); return { ptr: ptr, length: bytes.byteLength, free: function () { exports.free(ptr); } }; } function createJsString(text) { const ptr = exports.malloc(text.length * 2); const words = new Uint16Array(exports.memory.buffer, ptr, text.length); for (let i = 0; i < words.length; ++i) words[i] = text.charCodeAt(i); return { ptr: ptr, length: words.length, free: function () { exports.free(ptr); } }; } /** * Create an object representing a Harfbuzz buffer. **/ function createBuffer() { var ptr = exports.hb_buffer_create(); return { ptr: ptr, /** * Add text to the buffer. * @param {string} text Text to be added to the buffer. **/ addText: function (text) { const str = createJsString(text); exports.hb_buffer_add_utf16(ptr, str.ptr, str.length, 0, str.length); str.free(); }, /** * Set buffer script, language and direction. * * This needs to be done before shaping. **/ guessSegmentProperties: function () { return exports.hb_buffer_guess_segment_properties(ptr); }, /** * Set buffer direction explicitly. * @param {string} direction: One of "ltr", "rtl", "ttb" or "btt" */ setDirection: function (dir) { exports.hb_buffer_set_direction(ptr, { ltr: 4, rtl: 5, ttb: 6, btt: 7 }[dir] || 0); }, /** * Set buffer flags explicitly. * @param {string[]} flags: A list of strings which may be either: * "BOT" * "EOT" * "PRESERVE_DEFAULT_IGNORABLES" * "REMOVE_DEFAULT_IGNORABLES" * "DO_NOT_INSERT_DOTTED_CIRCLE" */ setFlags: function (flags) { var flagValue = 0 flags.forEach(function (s) { flagValue |= _buffer_flag(s); }) exports.hb_buffer_set_flags(ptr,flagValue); }, /** * Set buffer language explicitly. * @param {string} language: The buffer language */ setLanguage: function (language) { var str = createCString(language); exports.hb_buffer_set_language(ptr, exports.hb_language_from_string(str.ptr,-1)); str.free(); }, /** * Set buffer script explicitly. * @param {string} script: The buffer script */ setScript: function (script) { var str = createCString(script); exports.hb_buffer_set_script(ptr, exports.hb_script_from_string(str.ptr,-1)); str.free(); }, /** * Set the Harfbuzz clustering level. * * Affects the cluster values returned from shaping. * @param {number} level: Clustering level. See the Harfbuzz manual chapter * on Clusters. **/ setClusterLevel: function (level) { exports.hb_buffer_set_cluster_level(ptr, level) }, /** * Return the buffer contents as a JSON object. * * After shaping, this function will return an array of glyph information * objects. Each object will have the following attributes: * * - g: The glyph ID * - cl: The cluster ID * - ax: Advance width (width to advance after this glyph is painted) * - ay: Advance height (height to advance after this glyph is painted) * - dx: X displacement (adjustment in X dimension when painting this glyph) * - dy: Y displacement (adjustment in Y dimension when painting this glyph) * - flags: Glyph flags like `HB_GLYPH_FLAG_UNSAFE_TO_BREAK` (0x1) **/ json: function () { var length = exports.hb_buffer_get_length(ptr); var result = []; var infosPtr = exports.hb_buffer_get_glyph_infos(ptr, 0); var infosPtr32 = infosPtr / 4; var positionsPtr32 = exports.hb_buffer_get_glyph_positions(ptr, 0) / 4; var infos = heapu32.subarray(infosPtr32, infosPtr32 + 5 * length); var positions = heapi32.subarray(positionsPtr32, positionsPtr32 + 5 * length); for (var i = 0; i < length; ++i) { result.push({ g: infos[i * 5 + 0], cl: infos[i * 5 + 2], ax: positions[i * 5 + 0], ay: positions[i * 5 + 1], dx: positions[i * 5 + 2], dy: positions[i * 5 + 3], flags: exports.hb_glyph_info_get_glyph_flags(infosPtr + i * 20) }); } return result; }, /** * Free the object. */ destroy: function () { exports.hb_buffer_destroy(ptr); } }; } /** * Shape a buffer with a given font. * * This returns nothing, but modifies the buffer. * * @param {object} font: A font returned from `createFont` * @param {object} buffer: A buffer returned from `createBuffer` and suitably * prepared. * @param {object} features: (Currently unused). */ function shape(font, buffer, features) { exports.hb_shape(font.ptr, buffer.ptr, 0, 0); } /** * Shape a buffer with a given font, returning a JSON trace of the shaping process. * * This function supports "partial shaping", where the shaping process is * terminated after a given lookup ID is reached. If the user requests the function * to terminate shaping after an ID in the GSUB phase, GPOS table lookups will be * processed as normal. * * @param {object} font: A font returned from `createFont` * @param {object} buffer: A buffer returned from `createBuffer` and suitably * prepared. * @param {object} features: A dictionary of OpenType features to apply. * @param {number} stop_at: A lookup ID at which to terminate shaping. * @param {number} stop_phase: Either 0 (don't terminate shaping), 1 (`stop_at` refers to a lookup ID in the GSUB table), 2 (`stop_at` refers to a lookup ID in the GPOS table). */ function shapeWithTrace(font, buffer, features, stop_at, stop_phase) { var bufLen = 1024 * 1024; var traceBuffer = exports.malloc(bufLen); var featurestr = createCString(features); var traceLen = exports.hbjs_shape_with_trace(font.ptr, buffer.ptr, featurestr.ptr, stop_at, stop_phase, traceBuffer, bufLen); featurestr.free(); var trace = utf8Decoder.decode(heapu8.subarray(traceBuffer, traceBuffer + traceLen - 1)); exports.free(traceBuffer); return JSON.parse(trace); } return { createBlob: createBlob, createFace: createFace, createFont: createFont, createBuffer: createBuffer, shape: shape, shapeWithTrace: shapeWithTrace }; }; // Should be replaced with something more reliable try { module.exports = hbjs; } catch(e) {} ================================================ FILE: examples/sites/coldtype.p5js/assets/script.js ================================================ print = console.log //'use strict'; var hb, fontBlob; function pathToRelative(pathArray) { return pathArray; if (!pathArray.length) return []; var x = pathArray[0][1], y = pathArray[0][2]; var prevCmd = ''; return [["M", x, y]].concat(pathArray.slice(1).map(function (pa) { var r = [prevCmd === pa[0] ? ' ' : pa[0].toLowerCase()].concat(pa.slice(1).map(function (z, i) { return z - ((i % 2) ? y : x); })); var lastPoint = r.slice(-2); x += lastPoint[0]; y += lastPoint[1]; prevCmd = pa[0]; return r; })); } class HBGlyph { constructor(x, y, path) { this.x = x; this.y = y; this.commands = path; } } class HBFont { constructor(fontURL, fontBlob) { this.fontURL = fontURL; this.fontBlob = fontBlob; } static async loadFont(_fontURL) { if (window.hb === undefined) { const wasmResponse = await fetch("assets/hb.wasm"); const wasmArrayBuffer = await wasmResponse.arrayBuffer(); const wasmResult = await WebAssembly.instantiate(wasmArrayBuffer); window.hb = hbjs(wasmResult.instance); } const fontResponse = await fetch(_fontURL); const fontArrayBuffer = await fontResponse.arrayBuffer(); return new HBFont(_fontURL, new Uint8Array(fontArrayBuffer)); } getGlyphs(text, size, variations={}) { var blob = hb.createBlob(this.fontBlob); var face = hb.createFace(blob, 0); var os2 = face.reference_table("OS/2"); console.log(os2); var string = new TextDecoder().decode(os2); console.log(string); var font = hb.createFont(face); font.setScale(size, size); // Optional, if not given will be in font upem font.setVariations(variations); var buffer = hb.createBuffer(); buffer.addText(text); buffer.guessSegmentProperties(); // buffer.setDirection('ltr'); // optional as can be by guessSegmentProperties also hb.shape(font, buffer); // features are not supported yet var result = buffer.json(font); // returns glyphs paths, totally optional var glyphs = {}; result.forEach(function (x) { if (glyphs[x.g]) return; glyphs[x.g] = font.glyphToJson(x.g); }); buffer.destroy(); font.destroy(); face.destroy(); blob.destroy(); var hb_glyphs = []; var ax = 0; var ay = 0; result.map((x) => { let fcs = []; glyphs[x.g].map((v) => { let fc = {type:g.type}; if (g.values) { g.values.forEach((v, idx) => { let i = Math.floor(idx/2); if (idx%2 == 0) { fc[`x${i==0?'':i}`] = v; } else { fc[`y${i==0?'':i}`] = v; } }); } fcs.push(fc); }); hb_glyphs.push(new HBGlyph(ax+x.dx, ay+x.dy, fcs)); ax += x.ax; ay += x.ay; }); var xmin = 10000; var xmax = -10000; var ymin = 10000; var ymax = -10000; ax = 0; ay = 0; var path = pathToRelative(result.map(function (x) { var result = glyphs[x.g].filter(function (command) { return command.type !== 'Z'; }).map(function (command) { var result = command.values.map(function (p, i) { return i % 2 ? -(p + ay + x.dy) : p + ax + x.dx; }).map(function (x, i) { // bbox calc if (i % 2) { if (x < ymin) ymin = x; if (x > ymax) ymax = x; } else { if (x < xmin) xmin = x; if (x > xmax) xmax = x; } return x; }); return [command.type].concat(result); }); ax += x.ax; ay += x.ay; return result; }).reduce((acc, val) => acc.concat(val), [])).map(x => x[0] + x.slice(1).join(' ')).join('').replace(/ -/g, ' -'); var width = xmax - xmin; var height = ymax - ymin; var bbox = xmin + ' ' + ymin + ' ' + width + ' ' + height; svgResult.innerHTML = '' + ''; // var hb_glyphs = []; // var ax = 0; // var ay = 0; // result.map((x) => { // let commands = []; // glyphs[x.g].map((g) => { // commands.push(g); // }); // hb_glyphs.push(new HBGlyph(ax+x.dx, ay+x.dy, commands)); // ax += x.ax; // ay += x.ay; // }); // return hb_glyphs; } } let font; let hbFont; let textInput; let sampleFactorSlider; let zSlider; let animateCheckbox; let bounds; let zPos = 0; function updateResult() { let paths = hbFont.getGlyphs("abc", 1000, {wght:900,opsz:72}); console.log(paths); } async function preload() { // console.log("preload"); let fontName = "assets/fonts/PolymathVar.woff2"; // font = loadFont(fontName); hbFont = await HBFont.loadFont(fontName); //console.log(">", hbFont); updateResult(); opentype.load(fontName, function(err, f) { if (err) { console.log(err); } else { font = f; } fontPath = font.getPath("a", 0, 0, 100); console.log(fontPath); }); } function setup() { //createCanvas(windowWidth, windowHeight, WEBGL); return; textInput = createInput("abc"); createElement("br"); sampleFactorSlider = createSlider(0.01, 10, 2, 0.01); createElement("br"); zSlider = createSlider(0, 1200, 0); animateCheckbox = createCheckbox("animate", true); // animateCheckbox.changed(autoAnimate); textInput.position(8, 8); sampleFactorSlider.position(8, 32); zSlider.position(8, 52); animateCheckbox.position(8, 72); } function draw() { return; if (hbFont == undefined) { return; } background(0); // animate automatically //autoAnimate(); orbitControl(); word = " " + textInput.value() + " "; //console.log(font); let pts = font.textToPoints(word, 0, 0, 10, { sampleFactor: sampleFactorSlider.value()*0.5, simplifyThreshold: 0, }); let bounds = font.textBounds(word, 0, 0, 10); let multiplier = width / bounds.w; translate(-width / 4 - (bounds.w * multiplier) / 4, 0); translate(0, (bounds.h * multiplier) / 2); randomSeed(0); for (let i = 0; i < pts.length; i++) { stroke(255); point( pts[i].x * multiplier, pts[i].y * multiplier, zPos - zSlider.value() * random(0, 2.5) ); } // let glyphs = hbFont.getGlyphs("ASDF", 1000, {wght:100}); // background(250); // glyphs.forEach((glyph) => { // let points = []; // let path = new g.Path(glyph.commands); // path = g.resampleByLength(path, 5); // for (let i=0; i{{ fonts[0].fonts[0].woff2_relative }}

-->

""" footer: jinja_html = """ """ style: css = """ * { box-sizing: border-box; } body { text-align: center; background: black; } h1 { font-variation-settings: "wght" 900; } h2 { font-variation-settings: "wght" 700; } header a { color: royalblue; } header a.current { color: hotpink; } main { margin: 20px; } """ info = dict( title="Coldtype/p5js Experiment", description="An experiment", style=style, #script=script, font_name=font, scripts=[ "https://unpkg.com/canvaskit-wasm@0.19.0/bin/canvaskit.js", #"https://cdn.rawgit.com/nodebox/g.js/master/dist/g.min.js", "https://cdn.jsdelivr.net/gh/generative-design/Code-Package-p5.js@master/libraries/gg-dep-bundle/gg-dep-bundle.js", "https://cdnjs.cloudflare.com/ajax/libs/opentype.js/0.7.3/opentype.min.js", "assets/hbjs.js", "assets/p5.min.js", "assets/script.js", ], templates=dict(_header=header, _footer=footer, index=index), ) @site(ººsiblingºº(".") , port=8008 , info=info , fonts={ "text-font": dict(regular=font, _features=dict(ss01=True,ss02=True)) }) def website(_): website.build() def release(_): website.upload("example.com", "us-west-1", "personal") ================================================ FILE: examples/sites/coldtype.xyz/coldtype.xyz.py ================================================ from coldtype import * from coldtype.web.site import * header: jinja_html = """

Coldtype

""" index: jinja_html = """

Coldtype makes tools for advanced typography (and some other related stuff).


Currently those tools include:

  • coldtype: an open-source Python library for programming animated display typography
  • ST2: an open-source Blender add-on for doing good typography in 3D
  • b3denv: an open-source command-line utility for working with Blender and its embedded Python
  • Nudge: a VS Code extension that allows keyboard shortcuts to increment/decrement numeric values (and letters) and then immediately save the file

Here are some talks about Coldtype & ST2:

""" footer: jinja_html = """

Coldtype is a project of Goodhertz;
development is led by Rob Stenson.

""" style: css = """ * { box-sizing: border-box; } :root { --border-color: #ddd; } html, body { height: 100%; } body { background: white; color: #222; font-family: var(--text-font); display: flex; flex-direction: column; } em { font-style: normal; --text-font: fvs(ital=1); } a { color: royalblue; text-decoration: none; } h1 { --text-font: fvs(wght=1); } header, footer { background: white; } header { border-bottom: 1px solid var(--border-color); } footer { border-top: 1px solid var(--border-color); } header, footer, main { flex: 1; padding: 30px 20px; display: flex; justify-content: center; align-items: center; } header, footer { text-align: center; max-height: 200px; min-height: 200px; } main { padding: 50px 20px; } .wrapper { max-width: 500px; } .link-list { list-style: none; display: flex; flex-wrap: wrap; margin-top: 6px; justify-content: center; row-gap: 12px; } .link-list li:not(:last-child)::after { content: "///"; color: #ccc; } .link-list li a { padding: 4px 7px; background: whitesmoke; } .link-list li a:hover { background: lightpink; } main p { margin-bottom: 16px; } main strong { --text-font: fvs(wght=0.7); } main hr { border: none; border-top: 2px solid #eee; margin-bottom: 20px; margin: 26px 60px 30px; } main ul { margin-left: 20px; } main li { margin-bottom: 10px; } main li a { --text-font: fvs(wght=0.75); } """ info = dict( title="Coldtype", description="Coldtype is a programming library for advanced typography (and other stuff)", style=style, templates=dict(_header=header, _footer=footer, index=index), externals={ "github": "https://github.com/coldtype", "youtube": "https://www.youtube.com/channel/UCIRaiGAVFaM-pSErJG1UZFA", "tutorials": "https://coldtype.goodhertz.com/", # "q&a forum": "https://github.com/goodhertz/coldtype/discussions", }, ) @renderable(preview_scale=0.25) def favicon(r): return StSt("C", Font.ColdObvi(), 1300, wdth=1).align(r).f(0) @site(ººsiblingºº(".") , port=8008 , livereload=True , info=info , favicon=favicon , fonts={"text-font": dict(regular="MDIO-VF")}) def website(_): website.build() @renderable((1080, 200), fmt="png") def logo(r): return (Glyphwise("COLDTYPE", lambda g: Style(Font.ColdObvi(), 200, wdth=ez(g.e, "l", 1, rng=(1, 0)))) .f(0) .align(r)) def release(_): website.upload("coldtype.xyz", "us-west-1", "personal") ================================================ FILE: examples/sites/portfolio/assets/style.css ================================================ body { background: lightgreen; font-family: var(--text-font), monospace; } a { color: royalblue; } ================================================ FILE: examples/sites/portfolio/build.py ================================================ from coldtype import * from coldtype.web.site import * def public_posts(site): return [p for p in site.pages if p.template == "_post"] info = dict( title="Coldtype Portfolio", description="This is the Coldtype portfolio", navigation={"Home": "/", "About": "/about"}) def pagemod(p): print(p.title) return p @site(ººsiblingºº(".") , port=8008 , sources=dict(public_posts=public_posts) , info=info , pagemod=pagemod , fonts={ "text-font": dict(regular="Casserole-Sans"), "display-font": dict(regular="CheeeVariable.ttf")}) def portfolio(_): portfolio.build() @renderable((1080, 540), bg=1) def test(r): return (StSt("TESTA", Font.MuSan(), 400).align(r)) @renderable((1080, 540), bg=1) def testb(r): return (StSt("TESTB", Font.MuSan(), 400).align(r)) ================================================ FILE: examples/sites/portfolio/pages/about.md ================================================ --- title: About random: value --- This is the __about__ page, right? Here is a link to an [example post](/example). ================================================ FILE: examples/sites/portfolio/pages/posts/example.md ================================================ --- title: Example Post slug: example date: 2024-06-28 --- ![An example](/renders/build_test.png) This is an example post. +++ ![Another example](/renders/build_testb.png) ================================================ FILE: examples/sites/portfolio/templates/_footer.j2 ================================================
This is the footer
================================================ FILE: examples/sites/portfolio/templates/_header.j2 ================================================

Coldtype

================================================ FILE: examples/sites/portfolio/templates/_page.j2 ================================================

{{ page.title }}

{{ page.content|safe }}
================================================ FILE: examples/sites/portfolio/templates/_post.j2 ================================================

Post: {{ page.title }}

{{ page.content|safe }}
================================================ FILE: examples/sites/portfolio/templates/index.j2 ================================================

Index

This is the index page of the Coldtype portfolio site

================================================ FILE: examples/sites/skeleton/skeleton.py ================================================ from coldtype import * from coldtype.web.site import * header: jinja_html = """

header

{{ nav_html|safe }} """ index: jinja_html = """

index page

""" about: jinja_html = """

about page

""" footer: jinja_html = """

footer

""" style: css = """ * { box-sizing: border-box; } body { text-align: center; } h1 { font-variation-settings: "wght" 900; } h2 { font-variation-settings: "wght" 700; } header a { color: royalblue; } header a.current { color: hotpink; } main { border: 1px solid black; margin: 20px; } """ script: js = """ console.log("hello world") """ info = dict( title="Skeleton", description="This is empty code to copy", style=style, script=script, templates=dict(_header=header, _footer=footer, index=index, about=about), navigation={"Home": "/", "About": "/about"}, ) @site(ººsiblingºº(".") , port=8008 , info=info , fonts={ "text-font": dict(regular="Cheee_Caption", _embed=True) }) def website(_): website.build() def release(_): website.upload("example.com", "us-west-1", "personal") ================================================ FILE: examples/skia_direct.py ================================================ from coldtype import * @skia_direct((500, 300)) def direct_to_canvas(r, canvas): canvas.drawString( "hello", 10, r.h-10, skia.Font(skia.Typeface("Times New Roman"), 150), skia.Paint(AntiAlias=True, Color=hsl(0.9, s=1).skia())) ================================================ FILE: examples/skia_paragraph.py ================================================ from coldtype import * from coldtype.fx.skia import draw_canvas from skia import textlayout, ColorBLACK, Paint, Unicodes, FontMgr, FontStyle, ColorBLUE # adaptation of https://github.com/HinTak/skia-python-examples/blob/main/skparagraph-example.py @animation((1080, 540), bg=1, tl=Timeline(30, 30)) def paragraph(f): def draw(r, canvas): font_collection = textlayout.FontCollection() font_collection.setDefaultFontManager(FontMgr()) strut_style = textlayout.StrutStyle() strut_style.setStrutEnabled(True) #strut_style.setLeading(f.e("eeio", rng=(3.05, 0.75))) strut_style.setLeading(0) para_style = textlayout.ParagraphStyle() para_style.setStrutStyle(strut_style) builder = textlayout.ParagraphBuilder.make(para_style, font_collection, Unicodes.ICU.Make()) paint = Paint() paint.setAntiAlias(True) paint.setColor(ColorBLACK) style = textlayout.TextStyle() style.setFontSize(30.0) style.setForegroundPaint(paint) style.setFontFamilies(["hex franklin v0.3 narrow", "times", "georgia", "serif"]) builder.pushStyle(style) style_bold = style.cloneForPlaceholder() style_bold.setFontStyle(FontStyle.Bold()) builder.pushStyle(style_bold) builder.addText("Typography") builder.pop() builder.addText(" is the ") style_italic = style.cloneForPlaceholder() style_italic.setFontStyle(FontStyle.Italic()) #style_italic.setLetterSpacing(-2.0) style_italic.setWordSpacing(-10) paint.setColor(ColorBLUE) style_italic.setForegroundPaint(paint) builder.pushStyle(style_italic) builder.addText("art and technique") builder.pop() builder.addText(" of arranging type to make written language ") style_underline = style.cloneForPlaceholder() style_underline.setDecoration(textlayout.TextDecoration.kUnderline) style_underline.setDecorationMode(textlayout.TextDecorationMode.kGaps) style_underline.setDecorationColor(ColorBLACK) style_underline.setDecorationThicknessMultiplier(1.5) builder.pushStyle(style_underline) builder.addText("legible, readable, and appealing") builder.pop() builder.addText(" when displayed. The arrangement of type involves selecting typefaces, point sizes, line lengths, line-spacing (leading), and letter-spacing (tracking), and adjusting the space between pairs of letters (kerning). The term typography is also applied to the style, arrangement, and appearance of the letters, numbers, and symbols created by the process.") builder.addText(" Furthermore, العربية نص جميل. द क्विक ब्राउन फ़ॉक्स jumps over the lazy 🐕.") ri = r.inset(f.e("eeio", rng=(10, 300))) paragraph = builder.Build() paragraph.layout(ri.w) width = paragraph.LongestLine height = paragraph.Height paragraph.paint(canvas, r.w/2 - width/2, r.h/2 - height/2) return draw_canvas(f.a.r, draw) ================================================ FILE: examples/skia_shader.py ================================================ from coldtype import * from coldtype.fx.skia import rasterized, image_shader r = Rect(540) image = StSt("TYPE", Font.ColdObvi(), 500, wdth=0).align(r).ch(rasterized(r)) @animation(r, tl=Timeline(60, 30), bg=1) def warper2(f): sksl = f""" uniform shader image; half4 main(float2 coord) {{ coord.x += sin(coord.y / {f.e("eeio", rng=(40, 10))}) * {f.e("eeio", rng=(60, 0))}; return image.eval(coord); }}""" return P().ch(image_shader(f.a.r, image, sksl)) ================================================ FILE: examples/skia_shader.sksl ================================================ // kind=shader float rand(float n){return fract(sin(n) * 43758.5453123);} float noise(float p){ float fl = floor(p); float fc = fract(p); return mix(rand(fl), rand(fl + 1.0), fc); } float noise(vec2 n) { const vec2 d = vec2(0.0, 1.0); vec2 b = floor(n), f = smoothstep(vec2(0.0), vec2(1.0), fract(n)); return mix(mix(rand(b), rand(b + d.yx), f.x), mix(rand(b + d.xy), rand(b + d.yy), f.x), f.y); } ================================================ FILE: examples/skia_shader2.py ================================================ from coldtype import * from coldtype.fx.skia import rasterized, image_shader r = Rect(540) image = StSt("TYPE", Font.ColdObvi(), 500, wdth=0).align(r).ch(rasterized(r)) @animation(r, tl=Timeline(60, 30), bg=1) def warper2(f): sksl = f""" float hash(float2 p) {{ return fract(sin(dot(p, float2(127.1, 311.7))) * 43758.5453123); }} float noise(float2 p) {{ float2 i = floor(p); float2 f = fract(p); float a = hash(i); float b = hash(i + float2(1.0, 0.0)); float c = hash(i + float2(0.0, 1.0)); float d = hash(i + float2(1.0, 1.0)); float2 u = f * f * (3.0 - 2.0 * f); return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; }} float perlinNoise(float2 p, float persistence) {{ float total = 0.0; float amplitude = 1.0; float frequency = 1.0; float maxValue = 0.0; for (int i = 0; i < 4; ++i) {{ total += noise(p * frequency) * amplitude; maxValue += amplitude; amplitude *= persistence; frequency *= 2.0; }} return total / maxValue; }} // Main shader program uniform shader image; uniform float time; // Time for animation //uniform float2 resolution; // Resolution of the texture //uniform float strength; // Strength of displacement float2 resolution = float2(1080, 1080); float strength = 1.0; half4 main(float2 coord) {{ float2 uv = coord / resolution; // Generate Perlin noise float noiseValueX = perlinNoise(uv + float2(time, 0.0), 0.5); float noiseValueY = perlinNoise(uv + float2(0.0, time), 0.5); // Displace UVs float2 displacement = float2(noiseValueX, noiseValueY) * strength; float2 displacedUV = uv + displacement; // Sample the texture return image.eval(displacedUV * resolution); }}""" return P().ch(image_shader(f.a.r, image, sksl)) ================================================ FILE: examples/skia_shader_clouds.sksl ================================================ // kind=shader uniform float3 iResolution; // Viewport resolution (pixels) uniform float iTime; // Shader playback time (s) uniform float4 iMouse; // Mouse drag pos=.xy Click pos=.zw (pixels) uniform float3 iImageResolution; // iImage1 resolution (pixels) uniform shader iImage1; // An input image. const float cloudscale = 1.1; const float speed = 0.03; const float clouddark = 0.5; const float cloudlight = 0.3; const float cloudcover = 0.2; const float cloudalpha = 8.0; const float skytint = 0.5; const vec3 skycolour1 = vec3(0.2, 0.4, 0.6); const vec3 skycolour2 = vec3(0.4, 0.7, 1.0); const mat2 m = mat2( 1.6, 1.2, -1.2, 1.6 ); vec2 hash( vec2 p ) { p = vec2(dot(p,vec2(127.1,311.7)), dot(p,vec2(269.5,183.3))); return -1.0 + 2.0*fract(sin(p)*43758.5453123); } float noise( in vec2 p ) { const float K1 = 0.366025404; // (sqrt(3)-1)/2; const float K2 = 0.211324865; // (3-sqrt(3))/6; vec2 i = floor(p + (p.x+p.y)*K1); vec2 a = p - i + (i.x+i.y)*K2; vec2 o = (a.x>a.y) ? vec2(1.0,0.0) : vec2(0.0,1.0); //vec2 of = 0.5 + 0.5*vec2(sign(a.x-a.y), sign(a.y-a.x)); vec2 b = a - o + K2; vec2 c = a - 1.0 + 2.0*K2; vec3 h = max(0.5-vec3(dot(a,a), dot(b,b), dot(c,c) ), 0.0 ); vec3 n = h*h*h*h*vec3( dot(a,hash(i+0.0)), dot(b,hash(i+o)), dot(c,hash(i+1.0))); return dot(n, vec3(70.0)); } float fbm(vec2 n) { float total = 0.0, amplitude = 0.1; for (int i = 0; i < 7; i++) { total += noise(n) * amplitude; n = m * n; amplitude *= 0.4; } return total; } // ----------------------------------------------- half4 main(in vec2 fragCoord ) { vec2 p = fragCoord.xy / iResolution.xy; vec2 uv = p*vec2(iResolution.x/iResolution.y,1.0); float time = iTime * speed; float q = fbm(uv * cloudscale * 0.5); //ridged noise shape float r = 0.0; uv *= cloudscale; uv -= q - time; float weight = 0.8; for (int i=0; i<8; i++){ r += abs(weight*noise( uv )); uv = m*uv + time; weight *= 0.7; } //noise shape float f = 0.0; uv = p*vec2(iResolution.x/iResolution.y,1.0); uv *= cloudscale; uv -= q - time; weight = 0.7; for (int i=0; i<8; i++){ f += weight*noise( uv ); uv = m*uv + time; weight *= 0.6; } f *= r + f; //noise colour float c = 0.0; time = iTime * speed * 2.0; uv = p*vec2(iResolution.x/iResolution.y,1.0); uv *= cloudscale*2.0; uv -= q - time; weight = 0.4; for (int i=0; i<7; i++){ c += weight*noise( uv ); uv = m*uv + time; weight *= 0.6; } //noise ridge colour float c1 = 0.0; time = iTime * speed * 3.0; uv = p*vec2(iResolution.x/iResolution.y,1.0); uv *= cloudscale*3.0; uv -= q - time; weight = 0.4; for (int i=0; i<7; i++){ c1 += abs(weight*noise( uv )); uv = m*uv + time; weight *= 0.6; } c += c1; vec3 skycolour = mix(skycolour2, skycolour1, p.y); vec3 cloudcolour = vec3(1.1, 1.1, 0.9) * clamp((clouddark + cloudlight*c), 0.0, 1.0); f = cloudcover + cloudalpha*f*r; vec3 result = mix(skycolour, clamp(skytint * skycolour + cloudcolour, 0.0, 1.0), clamp(f + c, 0.0, 1.0)); return vec4( result, 1.0 ); } ================================================ FILE: examples/snakes.py ================================================ from coldtype import * from coldtype.fx.skia import phototype, temptone, shake # a variation on https://github.com/djrrb/Python-for-Visual-Designers-Fall-2023/blob/main/session-4/challenges/snakes.py r = Rect(1000, 1000) s = Scaffold(r).numeric_grid(1, 8) rxs = random_series(-(d:=r.w/2-100), d, 0, mod=int, spread=200) def build_snake(seed): snake = P() snake.moveTo((0, 0)) prevX = 0 prevY = 0 for idx, x in enumerate(s): y = prevY + s[0].r.h if idx < len(s)-1: x = rxs[(idx+2)+seed*10] else: x = 0 snake.curveTo( (prevX, prevY+s[0].r.h), (x, y-s[0].r.h), (x, y)) prevX = x prevY = y return snake.endPath() @animation(r, bg=0, tl=Timeline(60, 10)) def snakes(f): return (P().enumerate(range(0, 7), lambda x: build_snake(x.el) .t(x.el*20, 0) .fssw(-1, 1, 5) .ch(shake(4, 2, seed=f.i))) .align(f.a.r) .ch(phototype(f.a.r.inset(-10), 4, 123, 17)) .postprocess(lambda res: res.ch(temptone(0.70, 0.10)))) ================================================ FILE: examples/spacing_clusters.py ================================================ from coldtype import * # https://brill.com/page/resources_fontsdownloads font = Font.Find("Brill-Bold") @renderable((540, 540), bg=0) def spaced_clusters(r:Rect): txt = (StSt("Ae̞ø̞", font, 200, cluster=True, postTracking=200) .f(1) .align(r, tx=1)) assert txt[-1].data("glyphName") == "oslash+uni031E" return (P(P(txt.copy().ambit()).f(hsl(0.9)), txt)) ================================================ FILE: examples/src_macro.py ================================================ from coldtype import * from coldtype.blender import * assert 4j == 0.1016 # see .coldtype.py for magic @renderable((500, 500), bg=1) def magic_trick(r): return (P( StSt("4j", Font.RecMono(), 100), StSt(str(4j), Font.RecMono(), 100)) .stack(20) .align(r) .f(0)) @b3d_runnable() def setup(bpw:BpyWorld): bpw.delete_previous() BpyObj.Monkey() ================================================ FILE: examples/stacking.py ================================================ from coldtype import * from coldtype.text.richtext import RichText txt = """ a few lines Of Text [b] right here etaoin [h] shrdlu [b] """ grafs = txt.strip().split("\n\n") @renderable() def stacking(r): def graf(txt): return (RichText(r, txt, lambda t, s: (t, Style("Trebuchet" if "h" in s else r"Georgia\.", 150 if "b" in s else 50))) .f(hsl(0.7))) return (P( graf(grafs[0]), P().oval(Rect(50, 50)).ups().f(hsl(0.07, 0.9)), graf(grafs[1]), graf(grafs[2]), P().oval(Rect(50, 50)).outline(10).ups().f(hsl(0.07, 0.9)), graf(grafs[3]) ) .stack(20, zero=1) .align(r) .map(lambda p: p .xalign(r) .extend((lambda p: (P(p.ambit(tx=0)) .fssw(-1, hsl(0.3, a=0.6), 2)), (P(p.ambit(tx=1, ty=1)) .fssw(-1, hsl(0.9, a=0.3), 2)))))) ================================================ FILE: examples/svg_viewer.py ================================================ from coldtype import * from coldtype.raster import * from coldtype.img.skiasvg import SkiaSVG import skia doc = """ """ # doc = """ # # """ dom = skia.SVGDOM.MakeFromStream(skia.MemoryStream(doc.encode('utf-8'))) dom.setContainerSize(skia.Size(1080, 1080)) @renderable() def scratch(r): return (SkiaSVG(dom)) ================================================ FILE: examples/transparency.py ================================================ from coldtype import * f = bw(0.9) @renderable(30, bg=1, render_bg=1) def blocks(r): s = Scaffold(r).numeric_grid(2, 2) return [P(s["0|1"]).f(f), P(s["1|0"]).f(f)] @renderable(1024, bg=-1) def appicon(r): return (StSt("C", Font.ColdObvi(), 1280) .align(r)) def release(_): from shutil import copy2 blocks_path = blocks.render_to_disk(render_bg=1)[0] copy2(blocks_path, Path("packages/coldtype-core/src/coldtype/demo") / blocks_path.name) icon_path = appicon.render_to_disk(render_bg=0)[0] copy2(icon_path, Path("packages/coldtype-core/src/coldtype/demo") / icon_path.name) ================================================ FILE: examples/transparent_understroke.py ================================================ from coldtype import * from functools import partial @renderable((1080, 680)) def understroke_cut(r): def cut(line, i, p): if i > 0: return p.difference(line[i-1].copy().outline(10, drawOuter=False)) return (StSt("COLD\nTYPE", Font.ColdObvi(), 300, tu=-100, ro=1) .f(hsl(0.9)) .map(lambda p: p.map(partial(cut, p))) .map(lambda p: p.track(50)) .align(r)) ================================================ FILE: examples/ufo.py ================================================ from coldtype import * fs = Font.Find(r"Cold.*_CompressedBlackItalic\.ufo", "assets") @renderable((1080, 540)) def scratch(r): return StSt("CDELOPTY", fs, 300).align(r) ================================================ FILE: examples/vector_pixels.py ================================================ from coldtype import * from coldtype.fx.skia import vector_pixels @renderable((1080, 540)) def scratch(r:Rect): return (StSt("ABC", Font.MuSan(), 500, wght=0.25) .align(r) .ch(vector_pixels(r, scale=0.05, combine=True, lut={ (0, 50, 100, 100): (c:=hsl(0.65)), (0, 37, 74, 74): c.lighter(0.1), (0, 25, 50, 50): c.lighter(0.2), (0, 12, 25, 25): c.lighter(0.3), })) .align(r) .print()) ================================================ FILE: examples/wip/capture.py ================================================ # a good invocation: coldtype examples/capture.py -wpass -wt -wo 0.85 -wp NW -ps 2 from coldtype import * from coldtype.capture import read_frame from coldtype.midi.controllers import LaunchControlXL Style.RegisterShorthandPrefix("≈", "~/Type/fonts/fonts") fnt = Font.Cacheable("≈/CheeeVariable.ttf") @animation((1080, 1080), cv2caps=[0], rstate=1) def capture_with_midi(f:Frame, rs): nxl = LaunchControlXL(rs.midi) img = (read_frame(rs.cv2caps[0]).align(f.a.r)) return P( img.a(0.25), (StSt("CTRL", fnt, 200+nxl(10, 0)*500, r=1, # reverse ro=1, # remove-overlap rotate=nxl(60, 0)*360, grvt=nxl(40, 0), yest=nxl(20, 0), tu=(1-nxl(30, 0))*-650) .align(f.a.r.take(0.75, "mxy")) .f(hsl(0.7, 1, 0.7)) .understroke(sw=nxl(50, 0)*100))) ================================================ FILE: examples/wip/displace_map.py ================================================ from coldtype import * from coldtype.img.skiaimage import SkiaImage from coldtype.fx.skia import warp import numpy as np #from skimage.transform import swirl @renderable((540, 540)) def gradient(r): return (P(r).f(Gradient.H(r, bw(1), bw(0)))) @renderable((540, 540)) def text(r): return (StSt("COLD", Font.ColdObvi(), 500, wdth=0).align(r)) @renderable((540, 540), solo=1, bg=1) def displace(r): def cubic_bezier_curve(t, P0, P1, P2, P3): return (1 - t)**3 * P0 + 3 * (1 - t)**2 * t * P1 + 3 * (1 - t) * t**2 * P2 + t**3 * P3 def vertical_shrink(coords): h = r.h x, y = coords.T t = y / h P0, P1, P2, P3 = -0.1, 0.25, 0.75, 1 y_new = cubic_bezier_curve(t, P0, P1, P2, P3) * h return np.vstack((x, y_new)).T return (P(r).f(1) .up() .append(P().gridlines(r).fssw(-1, 0, 2)) .ch(warp(r, vertical_shrink))) ================================================ FILE: examples/wip/drawbot_image.py ================================================ from coldtype import * from coldtype.img.drawbotimage import DrawBotImage # wip @renderable((540, 540/2), bg=hsl(0.3), render_only=1) def test_image(r): return StSt("HELLO", Font.MuSan(), 100, wght=1).align(r, ty=1).f(hsl(0.9, 0.6, 0.6)) @animation((540, 540)) def db_image(f): DrawBotImage(test_image.pass_path(0)) pass ================================================ FILE: examples/wip/google_font.py ================================================ from coldtype import * # if you're running this on windows, you'll # probably need to do an `export PYTHONUTF8=1` # in your shell before running it, or else # python won't be able to understand these strings pairs = [ ["Noto Sans Tamil", "ஐக்கிய நாடுகள்"], ["Noto Sans Balinese", "ᬏᬢᬤᬢᬶᬭᬶᬓ᭄ᬢᬫ᭄,"], ["Noto Sans Telugu", "ఈ స్వత్వములను"], ["Noto Sans Bengali", "গাসৈ সুবুঙানৗ উদাংয়ৈ"], ["Noto Sans Sundanese", "ᮞᮊᮥᮙ᮪ᮔ ᮏᮜ᮪ᮙ ᮌᮥᮘᮢᮌ᮪"] ] @animation((1080, 540), tl=len(pairs), bg=1) def nabla1(f): font_name, text = pairs[f.i] return (StSt(text, Font.GoogleFont(font_name) , fontSize=50) .align(f.a.r, tx=0, ty=1) .f(0)) ================================================ FILE: examples/wip/toggle.py ================================================ from coldtype import * from coldtype.midi.controllers import LaunchControlXL @animation((1080, 1080), bg=1, rstate=1) def render(f, rstate): nxl = LaunchControlXL(rstate.midi) toggle_state = nxl(10, 0) > 0.5 return (StSt( "ON" if toggle_state else "OFF" , font=Font.MutatorSans() , fontSize=500 if toggle_state else 100) .align(f.a.r)) ================================================ FILE: examples/wip/ui.py ================================================ from coldtype import * """ Proof-of-concept for @ui interface - Click anywhere to change interpolation - Hit spacebar then move mouse for mouse-move """ @ui((1000, 1000)) def cursor_interp(u): ri = u.r.inset(100) sx, sy = ri.ipos(u.c) return [ P(ri).fssw(-1, hsl(0.9, a=0.3), 2), (StSt("VARI\nABLE", Font.MutatorSans(), 150 , wdth=sx, wght=sy) .xalign(u.r) .lead(30) .align(u.r) .f(0))] ================================================ FILE: packages/coldtype/README.md ================================================ __⚠️🌋 Disclaimer: this library is alpha-quality; the API is subject to change 🌋⚠️__ --- # Coldtype _Hello and welcome to `coldtype`, an odd little library for programmatic typography, written for use on [Goodhertz](https://goodhertz.com) projects and [text animations](https://vimeo.com/robstenson)._ Here’s a quick example: ```python from coldtype import * @renderable((1580, 350)) def render(r): return P( P(r.inset(10)).outline(10).f(hsl(0.65)), StSt("COLDTYPE", Font.ColdtypeObviously() , fontSize=250 , wdth=1 , tu=-250 , r=1 , rotate=15) .align(r) .fssw(hsl(0.65), 1, 5, 1) .translate(0, 5)) ``` If you have [uv](https://docs.astral.sh/uv/) installed, saving that code in a file named test.py and running `uvx coldtype test.py` results in this image popping up on your screen in a dedicated window: ![An example](https://raw.githubusercontent.com/goodhertz/coldtype/main/examples/renders/simple_render.png) ## Documentation Check out [coldtype.xyz](https://coldtype.xyz) for instructions on installing and getting started with coldtype. ## More Examples The best way to get familiar with Coldtype is to look at and try modifying some example code, like the animating gif below. To try out this example and many more, check out the [examples/animation](https://github.com/goodhertz/coldtype/tree/main/examples/animations) directory in this repo. ## Contributing To get a development environment for Coldtype: ``` git clone https://github.com/coldtype/coldtype.git uv run ct examples/animations/808.py ``` To run tests, etc. ``` uv sync --extra dev ``` ### Using with Blender Big thing: match your the Python version in your uv installation (or venv generally-speaking) to the Python version embedded - Find the desired Blender version: - `uvx b3denv python --version` - Create a project with that version: - `uv init --python 3.XX` - Add Coldtype: - `uv add coldtype` - Verify Coldtype installation: - `uv run coldtype demoblender` - Try a Blender-enabled Coldtype file: - `uv run coldtype demoblender -p b3dlo` ================================================ FILE: packages/coldtype/pyproject.toml ================================================ [project] name = "coldtype" version = "0.13.6" dependencies = [ "coldtype-core==0.13.6", "glfw", "PyOpenGL", "skia-python", "skia-pathops", "numpy", "potracer", "requests>=2.32.4", "mido", "ufo2ft>=3.2.8", # pin for fg issue "coldtype-fontgoggles[more-fonts]==1.8.4.7", "tqdm", "pyobjc-core", "pyobjc-framework-Cocoa", "pyobjc-framework-CoreText", ] [project.urls] Homepage = "https://coldtype.xyz" Documentation = "https://coldtype.goodhertz.com" Repository = "https://github.com/coldtype/coldtype" Issues = "https://github.com/coldtype/coldtype/issues" Changelog = "https://github.com/coldtype/coldtype/blob/main/CHANGELOG.md" [project.scripts] coldtype = "coldtype.renderer:main" ct = "coldtype.renderer:main" ctb = "coldtype.renderer:main_b3d" [project.optional-dependencies] more-fonts = [ "ufo2ft>=3.2.8", # pin for fg issue "coldtype-fontgoggles[more-fonts]==1.8.4.7" ] viewer = [] drawbot = ["numpy"] unicode = ["unicodedata2"] audio = ["pyaudio", "soundfile"] website = [ "jinja2>=3.1.6", "python-frontmatter", "livereload", "Markdown>=3.8.1", "markdown-captions", "beautifulsoup4", "brotli>=1.2.0", "lxml", "pygments", "sourcetypes", "favicons", "tornado>=6.5.5" ] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/coldtype"] [tool.uv.sources] coldtype-core = { workspace = true } ================================================ FILE: packages/coldtype-core/MANIFEST.in ================================================ include src/coldtype/renderer/.coldtype.py ================================================ FILE: packages/coldtype-core/README.md ================================================ `coldtype-core` is all of the `coldtype` code without optional dependencies made non-optional by the standard `coldtype` library (since almost all use-cases for `coldtype` want those dependencies). If you’re using coldtype in an embedded context and have no need for a GUI counterpart, you can use `coldtype-core` directly. ================================================ FILE: packages/coldtype-core/pyproject.toml ================================================ [project] name = "coldtype-core" version = "0.13.6" description = "Functions for manual vectorized typesetting." readme = "README.md" authors = [ { name = "Rob Stenson", email = "rob.stenson@gmail.com" } ] requires-python = ">=3.11" dependencies = [ "fontTools>=4.60.2", "fontPens==0.2.4", "easing-functions==1.0.4", "b3denv>=0.0.22", "coldtype-fontgoggles==1.8.4.7", "skia-pathops", "defcon>=0.12.0", # for OutlinePen "uharfbuzz<0.47.0", # some kind of cached variations/memory bug introduced in 0.47.0 "show-in-file-manager>=1.1.6", ] [project.urls] Homepage = "https://coldtype.xyz" Documentation = "https://coldtype.goodhertz.com" Repository = "https://github.com/coldtype/coldtype" Issues = "https://github.com/coldtype/coldtype/issues" Changelog = "https://github.com/coldtype/coldtype/blob/main/CHANGELOG.md" [project.optional-dependencies] more-fonts = [ "ufo2ft>=3.2.8", # pin for fg issue "coldtype-fontgoggles[more-fonts]==1.8.4.7" ] drawbot = ["numpy"] unicode = ["unicodedata2"] notebook = ["skia-pathops", "skia-python", "potracer", "tdqm"] [project.scripts] coldtype = "coldtype.renderer:main" ct = "coldtype.renderer:main" ctb = "coldtype.renderer:main_b3d" [tool.setuptools] license-files = [] # https://github.com/astral-sh/uv/issues/9513#issuecomment-2519527822 include-package-data = true [tool.setuptools.package-data] "coldtype.renderer" = [".coldtype.py"] #[tool.uv.sources] #coldtype-fontgoggles = { path = "../fontgoggles", editable = true } [build-system] requires = ["setuptools >= 61.0", "wheel", "setuptools-scm >= 8.0"] build-backend = "setuptools.build_meta" [dependency-groups] dev = [ "pytest>=8.3.5", "pynput", "pymunk", "pygments", "pyaudio", "soundfile", "opentype-feature-freezer", "fontmake", "measurement", "pillow", "imagehash", "scikit-image", "noise", "mdx-linkify", "ipykernel", "drawbot @ git+https://github.com/typemytype/drawbot", "numpy<2.2" ] ================================================ FILE: packages/coldtype-core/src/coldtype/__init__.py ================================================ import math, sys, os, re from pathlib import Path # source: https://github.com/PixarAnimationStudios/USD/issues/1372 def monkeypatch_ctypes(): import ctypes.util, platform uname = os.uname() if uname.sysname == "Darwin" and uname.release >= "20.": real_find_library = ctypes.util.find_library def find_library(name): if name in {"OpenGL", "GLUT"}: # add more names here if necessary return f"/System/Library/Frameworks/{name}.framework/{name}" #elif name in {"freetype"}: # res = real_find_library(name) # if res.startswith("/usr/local") and platform.processor() == "arm": # return "/opt/homebrew/lib/libfreetype.dylib" return real_find_library(name) ctypes.util.find_library = find_library return # try: # monkeypatch_ctypes() # except AttributeError: # pass from coldtype.runon.path import P from coldtype.runon.scaffold import Scaffold from coldtype.text import * from coldtype.geometry import * from coldtype.color import * from coldtype.renderable import * try: from coldtype.renderer.reader import Programs except ModuleNotFoundError: pass from coldtype.helpers import loopidx, sibling, quick_ufo, cycle_idx, random_series, glyph_to_uni, uni_to_glyph, glyph_to_class, download, DefconFont, raw_ufo from coldtype.timing import * from coldtype.timing.easing import ez from coldtype.timing.nle.ascii import AsciiTimeline from coldtype.timing.midi import MidiTimeline from coldtype.img.blendmode import BlendMode from coldtype.grid import Grid name = "coldtype" __version__ = "0.13.6" # these will be redefined contextually __FILE__ = None __BLENDER__ = None __VERSION__ = {} __VERSIONS__ = {} __BLENDERING__ = False __sibling__ = lambda x: x __inputs__ = [] ººinputsºº = [] __config__ = [] ººconfigºº = [] __memory__ = [] __ui__ = {} ººuiºº = {} __as_config__ = False λ = None ººFILEºº = None ººBLENDERºº = None ººBLENDERINGºº = False def ººsiblingºº(x) -> Path: return x def noop(): return None ================================================ FILE: packages/coldtype-core/src/coldtype/__main__.py ================================================ from coldtype.renderer import main if __name__ == '__main__': main() ================================================ FILE: packages/coldtype-core/src/coldtype/assets/glyphNamesToUnicode.txt ================================================ # Glyph Name Formatted Unicode List - GNFUL # GlyphNameFormatter version 0.4 - git commit: 468 # Unicode version: Unicode 12.1.0 # Source code: https://github.com/LettError/glyphNameFormatter/tree/c6b4b20 # Generated on 2019 12 10 10:04:58 # # # Basic Latin space 0020 Zs exclam 0021 Po quotedbl 0022 Po numbersign 0023 Po dollar 0024 Sc percent 0025 Po ampersand 0026 Po quotesingle 0027 Po parenleft 0028 Ps parenright 0029 Pe asterisk 002A Po plus 002B Sm comma 002C Po hyphen 002D Pd period 002E Po slash 002F Po zero 0030 Nd one 0031 Nd two 0032 Nd three 0033 Nd four 0034 Nd five 0035 Nd six 0036 Nd seven 0037 Nd eight 0038 Nd nine 0039 Nd colon 003A Po semicolon 003B Po less 003C Sm equal 003D Sm greater 003E Sm question 003F Po at 0040 Po A 0041 Lu B 0042 Lu C 0043 Lu D 0044 Lu E 0045 Lu F 0046 Lu G 0047 Lu H 0048 Lu I 0049 Lu J 004A Lu K 004B Lu L 004C Lu M 004D Lu N 004E Lu O 004F Lu P 0050 Lu Q 0051 Lu R 0052 Lu S 0053 Lu T 0054 Lu U 0055 Lu V 0056 Lu W 0057 Lu X 0058 Lu Y 0059 Lu Z 005A Lu bracketleft 005B Ps backslash 005C Po bracketright 005D Pe asciicircum 005E Sk underscore 005F Pc grave 0060 Sk a 0061 Ll b 0062 Ll c 0063 Ll d 0064 Ll e 0065 Ll f 0066 Ll g 0067 Ll h 0068 Ll i 0069 Ll j 006A Ll k 006B Ll l 006C Ll m 006D Ll n 006E Ll o 006F Ll p 0070 Ll q 0071 Ll r 0072 Ll s 0073 Ll t 0074 Ll u 0075 Ll v 0076 Ll w 0077 Ll x 0078 Ll y 0079 Ll z 007A Ll braceleft 007B Ps bar 007C Sm braceright 007D Pe asciitilde 007E Sm # Latin-1 Supplement nbspace 00A0 Zs exclamdown 00A1 Po cent 00A2 Sc sterling 00A3 Sc currency 00A4 Sc yen 00A5 Sc brokenbar 00A6 So section 00A7 Po dieresis 00A8 Sk copyright 00A9 So ordfeminine 00AA Lo guillemetleft 00AB Pi logicalnot 00AC Sm hyphensoft 00AD Cf registered 00AE So macron 00AF Sk degree 00B0 So plusminus 00B1 Sm twosuperior 00B2 No threesuperior 00B3 No acute 00B4 Sk mu.math 00B5 Ll paragraph 00B6 Po periodcentered 00B7 Po cedilla 00B8 Sk onesuperior 00B9 No ordmasculine 00BA Lo guillemetright 00BB Pf onequarter 00BC No onehalf 00BD No threequarters 00BE No questiondown 00BF Po Agrave 00C0 Lu Aacute 00C1 Lu Acircumflex 00C2 Lu Atilde 00C3 Lu Adieresis 00C4 Lu Aring 00C5 Lu AE 00C6 Lu Ccedilla 00C7 Lu Egrave 00C8 Lu Eacute 00C9 Lu Ecircumflex 00CA Lu Edieresis 00CB Lu Igrave 00CC Lu Iacute 00CD Lu Icircumflex 00CE Lu Idieresis 00CF Lu Eth 00D0 Lu Ntilde 00D1 Lu Ograve 00D2 Lu Oacute 00D3 Lu Ocircumflex 00D4 Lu Otilde 00D5 Lu Odieresis 00D6 Lu multiply 00D7 Sm Oslash 00D8 Lu Ugrave 00D9 Lu Uacute 00DA Lu Ucircumflex 00DB Lu Udieresis 00DC Lu Yacute 00DD Lu Thorn 00DE Lu germandbls 00DF Ll agrave 00E0 Ll aacute 00E1 Ll acircumflex 00E2 Ll atilde 00E3 Ll adieresis 00E4 Ll aring 00E5 Ll ae 00E6 Ll ccedilla 00E7 Ll egrave 00E8 Ll eacute 00E9 Ll ecircumflex 00EA Ll edieresis 00EB Ll igrave 00EC Ll iacute 00ED Ll icircumflex 00EE Ll idieresis 00EF Ll eth 00F0 Ll ntilde 00F1 Ll ograve 00F2 Ll oacute 00F3 Ll ocircumflex 00F4 Ll otilde 00F5 Ll odieresis 00F6 Ll divide 00F7 Sm oslash 00F8 Ll ugrave 00F9 Ll uacute 00FA Ll ucircumflex 00FB Ll udieresis 00FC Ll yacute 00FD Ll thorn 00FE Ll ydieresis 00FF Ll # Latin Extended-A Amacron 0100 Lu amacron 0101 Ll Abreve 0102 Lu abreve 0103 Ll Aogonek 0104 Lu aogonek 0105 Ll Cacute 0106 Lu cacute 0107 Ll Ccircumflex 0108 Lu ccircumflex 0109 Ll Cdotaccent 010A Lu cdotaccent 010B Ll Ccaron 010C Lu ccaron 010D Ll Dcaron 010E Lu dcaron 010F Ll Dcroat 0110 Lu dcroat 0111 Ll Emacron 0112 Lu emacron 0113 Ll Ebreve 0114 Lu ebreve 0115 Ll Edotaccent 0116 Lu edotaccent 0117 Ll Eogonek 0118 Lu eogonek 0119 Ll Ecaron 011A Lu ecaron 011B Ll Gcircumflex 011C Lu gcircumflex 011D Ll Gbreve 011E Lu gbreve 011F Ll Gdotaccent 0120 Lu gdotaccent 0121 Ll Gcedilla 0122 Lu gcedilla 0123 Ll Hcircumflex 0124 Lu hcircumflex 0125 Ll Hbar 0126 Lu hbar 0127 Ll Itilde 0128 Lu itilde 0129 Ll Imacron 012A Lu imacron 012B Ll Ibreve 012C Lu ibreve 012D Ll Iogonek 012E Lu iogonek 012F Ll Idotaccent 0130 Lu dotlessi 0131 Ll IJ 0132 Lu ij 0133 Ll Jcircumflex 0134 Lu jcircumflex 0135 Ll Kcedilla 0136 Lu kcedilla 0137 Ll kra 0138 Ll Lacute 0139 Lu lacute 013A Ll Lcedilla 013B Lu lcedilla 013C Ll Lcaron 013D Lu lcaron 013E Ll Ldot 013F Lu ldot 0140 Ll Lslash 0141 Lu lslash 0142 Ll Nacute 0143 Lu nacute 0144 Ll Ncedilla 0145 Lu ncedilla 0146 Ll Ncaron 0147 Lu ncaron 0148 Ll napostrophe 0149 Ll Eng 014A Lu eng 014B Ll Omacron 014C Lu omacron 014D Ll Obreve 014E Lu obreve 014F Ll Ohungarumlaut 0150 Lu ohungarumlaut 0151 Ll OE 0152 Lu oe 0153 Ll Racute 0154 Lu racute 0155 Ll Rcedilla 0156 Lu rcedilla 0157 Ll Rcaron 0158 Lu rcaron 0159 Ll Sacute 015A Lu sacute 015B Ll Scircumflex 015C Lu scircumflex 015D Ll Scedilla 015E Lu scedilla 015F Ll Scaron 0160 Lu scaron 0161 Ll Tcedilla 0162 Lu tcedilla 0163 Ll Tcaron 0164 Lu tcaron 0165 Ll Tbar 0166 Lu tbar 0167 Ll Utilde 0168 Lu utilde 0169 Ll Umacron 016A Lu umacron 016B Ll Ubreve 016C Lu ubreve 016D Ll Uring 016E Lu uring 016F Ll Uhungarumlaut 0170 Lu uhungarumlaut 0171 Ll Uogonek 0172 Lu uogonek 0173 Ll Wcircumflex 0174 Lu wcircumflex 0175 Ll Ycircumflex 0176 Lu ycircumflex 0177 Ll Ydieresis 0178 Lu Zacute 0179 Lu zacute 017A Ll Zdotaccent 017B Lu zdotaccent 017C Ll Zcaron 017D Lu zcaron 017E Ll longs 017F Ll # Latin Extended-B bstroke 0180 Ll Bhook 0181 Lu Btopbar 0182 Lu btopbar 0183 Ll Tonesix 0184 Lu tonesix 0185 Ll Oopen 0186 Lu Chook 0187 Lu chook 0188 Ll Dafrican 0189 Lu Dhook 018A Lu Dtopbar 018B Lu dtopbar 018C Ll deltaturned 018D Ll Ereversed 018E Lu Schwa 018F Lu Eopen 0190 Lu Fhook 0191 Lu fhook 0192 Ll Ghook 0193 Lu lt:Gamma 0194 Lu hv 0195 Ll lt:Iota 0196 Lu Istroke 0197 Lu Khook 0198 Lu khook 0199 Ll lbar 019A Ll lambdastroke 019B Ll Mturned 019C Lu Nhookleft 019D Lu nlongrightleg 019E Ll Obar 019F Lu Ohorn 01A0 Lu ohorn 01A1 Ll Oi 01A2 Lu oi 01A3 Ll Phook 01A4 Lu phook 01A5 Ll yr 01A6 Lu Tonetwo 01A7 Lu tonetwo 01A8 Ll Esh 01A9 Lu eshreversedloop 01AA Ll tpalatalhook 01AB Ll Thook 01AC Lu thook 01AD Ll Tretroflexhook 01AE Lu Uhorn 01AF Lu uhorn 01B0 Ll lt:Upsilon 01B1 Lu Vhook 01B2 Lu Yhook 01B3 Lu yhook 01B4 Ll Zstroke 01B5 Lu zstroke 01B6 Ll Ezh 01B7 Lu Ezhreversed 01B8 Lu ezhreversed 01B9 Ll ezhtail 01BA Ll twostroke 01BB Lo Tonefive 01BC Lu tonefive 01BD Ll glottalinvertedstroke 01BE Ll wynn 01BF Ll clickdental 01C0 Lo clicklateral 01C1 Lo clickalveolar 01C2 Lo clickretroflex 01C3 Lo DZcaron 01C4 Lu Dzcaron 01C5 Lt dzcaron 01C6 Ll LJ 01C7 Lu Lj 01C8 Lt lj 01C9 Ll NJ 01CA Lu Nj 01CB Lt nj 01CC Ll Acaron 01CD Lu acaron 01CE Ll Icaron 01CF Lu icaron 01D0 Ll Ocaron 01D1 Lu ocaron 01D2 Ll Ucaron 01D3 Lu ucaron 01D4 Ll Udieresismacron 01D5 Lu udieresismacron 01D6 Ll Udieresisacute 01D7 Lu udieresisacute 01D8 Ll Udieresiscaron 01D9 Lu udieresiscaron 01DA Ll Udieresisgrave 01DB Lu udieresisgrave 01DC Ll eturned 01DD Ll Adieresismacron 01DE Lu adieresismacron 01DF Ll Adotmacron 01E0 Lu adotmacron 01E1 Ll AEmacron 01E2 Lu aemacron 01E3 Ll Gstroke 01E4 Lu gstroke 01E5 Ll Gcaron 01E6 Lu gcaron 01E7 Ll Kcaron 01E8 Lu kcaron 01E9 Ll Oogonek 01EA Lu oogonek 01EB Ll Oogonekmacron 01EC Lu oogonekmacron 01ED Ll Ezhcaron 01EE Lu ezhcaron 01EF Ll jcaron 01F0 Ll DZ 01F1 Lu Dz 01F2 Lt dz 01F3 Ll Gacute 01F4 Lu gacute 01F5 Ll Hwair 01F6 Lu Wynn 01F7 Lu Ngrave 01F8 Lu ngrave 01F9 Ll Aringacute 01FA Lu aringacute 01FB Ll AEacute 01FC Lu aeacute 01FD Ll Oslashacute 01FE Lu oslashacute 01FF Ll Agravedbl 0200 Lu agravedbl 0201 Ll Ainvertedbreve 0202 Lu ainvertedbreve 0203 Ll Egravedbl 0204 Lu egravedbl 0205 Ll Einvertedbreve 0206 Lu einvertedbreve 0207 Ll Igravedbl 0208 Lu igravedbl 0209 Ll Iinvertedbreve 020A Lu iinvertedbreve 020B Ll Ogravedbl 020C Lu ogravedbl 020D Ll Oinvertedbreve 020E Lu oinvertedbreve 020F Ll Rgravedbl 0210 Lu rgravedbl 0211 Ll Rinvertedbreve 0212 Lu rinvertedbreve 0213 Ll Ugravedbl 0214 Lu ugravedbl 0215 Ll Uinvertedbreve 0216 Lu uinvertedbreve 0217 Ll Scommaaccent 0218 Lu scommaaccent 0219 Ll Tcommaaccent 021A Lu tcommaaccent 021B Ll Yogh 021C Lu yogh 021D Ll Hcaron 021E Lu hcaron 021F Ll Nlongrightleg 0220 Lu dcurl 0221 Ll Ou 0222 Lu ou 0223 Ll Zhook 0224 Lu zhook 0225 Ll Adot 0226 Lu adot 0227 Ll Ecedilla 0228 Lu ecedilla 0229 Ll Odieresismacron 022A Lu odieresismacron 022B Ll Otildemacron 022C Lu otildemacron 022D Ll Odot 022E Lu odot 022F Ll Odotmacron 0230 Lu odotmacron 0231 Ll Ymacron 0232 Lu ymacron 0233 Ll lcurl 0234 Ll ncurl 0235 Ll tcurl 0236 Ll dotlessj 0237 Ll dbdigraph 0238 Ll qpdigraph 0239 Ll Astroke 023A Lu Cstroke 023B Lu cstroke 023C Ll Lbar 023D Lu Twithdiagonalstroke 023E Lu sswashtail 023F Ll zswashtail 0240 Ll Glottalstop 0241 Lu glottalstop 0242 Ll Bstroke 0243 Lu Ubar 0244 Lu Vturned 0245 Lu Estroke 0246 Lu estroke 0247 Ll Jstroke 0248 Lu jstroke 0249 Ll Qsmallhooktail 024A Lu qhooktail 024B Ll Rstroke 024C Lu rstroke 024D Ll Ystroke 024E Lu ystroke 024F Ll # IPA Extensions aturned 0250 Ll ipa:alpha 0251 Ll alphaturned 0252 Ll bhook 0253 Ll oopen 0254 Ll ccurl 0255 Ll dtail 0256 Ll dhook 0257 Ll ipa:ereversed 0258 Ll schwa 0259 Ll schwahook 025A Ll eopen 025B Ll eopenreversed 025C Ll eopenreversedhook 025D Ll eopenreversedclosed 025E Ll dotlessjstroke 025F Ll ghook 0260 Ll ipa:gscript 0261 Ll Gsmall 0262 Ll ipa:gamma 0263 Ll ramshorn 0264 Ll hturned 0265 Ll hhook 0266 Ll henghook 0267 Ll istroke 0268 Ll ipa:iota 0269 Ll ipa:Ismall 026A Ll lmiddletilde 026B Ll lbelt 026C Ll lretroflex 026D Ll lezh 026E Ll mturned 026F Ll mlonglegturned 0270 Ll mhook 0271 Ll nhookleft 0272 Ll nretroflex 0273 Ll Nsmall 0274 Ll obarred 0275 Ll OEsmall 0276 Ll omegaclosed 0277 Ll ipa:phi 0278 Ll rturned 0279 Ll rlonglegturned 027A Ll rhookturned 027B Ll rlongleg 027C Ll ipa:rtail 027D Ll rfishhook 027E Ll rfishhookreversed 027F Ll Rsmall 0280 Ll Rinvertedsmall 0281 Ll shook 0282 Ll esh 0283 Ll dotlessjstrokehook 0284 Ll eshsquatreversed 0285 Ll eshcurl 0286 Ll tturned 0287 Ll tretroflex 0288 Ll ubar 0289 Ll ipa:upsilon 028A Ll vhook 028B Ll vturned 028C Ll wturned 028D Ll yturned 028E Ll Ysmall 028F Ll zretroflex 0290 Ll zcurl 0291 Ll ezh 0292 Ll ezhcurl 0293 Ll ipa:glottalstop 0294 Lo pharyngealvoicedfricative 0295 Ll glottalstopinverted 0296 Ll Cstretched 0297 Ll clickbilabial 0298 Ll Bsmall 0299 Ll eopenclosed 029A Ll Ghooksmall 029B Ll Hsmall 029C Ll jcrossedtail 029D Ll kturned 029E Ll Lsmall 029F Ll qhook 02A0 Ll glottalstopstroke 02A1 Ll glottalstopstrokereversed 02A2 Ll dzed 02A3 Ll dezh 02A4 Ll dzedcurl 02A5 Ll ts 02A6 Ll tesh 02A7 Ll tccurl 02A8 Ll feng 02A9 Ll ls 02AA Ll lzed 02AB Ll percussivebilabial 02AC Ll percussivebidental 02AD Ll hfishhookturned 02AE Ll handtailfishhookturned 02AF Ll # Spacing Modifier Letters hsupmod 02B0 Lm hhooksupmod 02B1 Lm jsupmod 02B2 Lm rsupmod 02B3 Lm rturnedsupmod 02B4 Lm rhookturnedsupmod 02B5 Lm Rsupinvertedmod 02B6 Lm wsupmod 02B7 Lm ysupmod 02B8 Lm primemod 02B9 Lm primedblmod 02BA Lm commaturnedmod 02BB Lm apostrophemod 02BC Lm commareversedmod 02BD Lm ringhalfrightmod 02BE Lm ringhalfleftmod 02BF Lm glottalstopmod 02C0 Lm glottalstopreversedmod 02C1 Lm arrowheadleftmod 02C2 Sk arrowheadrightmod 02C3 Sk arrowheadupmod 02C4 Sk arrowheaddownmod 02C5 Sk circumflex 02C6 Lm caron 02C7 Lm verticallinemod 02C8 Lm macronmod 02C9 Lm acutemod 02CA Lm gravemod 02CB Lm verticallinelowmod 02CC Lm macronlowmod 02CD Lm gravelowmod 02CE Lm acutelowmod 02CF Lm colontriangularmod 02D0 Lm colontriangularhalfmod 02D1 Lm ringhalfrightcentredmod 02D2 Sk ringhalfleftcentredmod 02D3 Sk tackupmod 02D4 Sk tackdownmod 02D5 Sk plussignmod 02D6 Sk minussignmod 02D7 Sk breve 02D8 Sk dotaccent 02D9 Sk ring 02DA Sk ogonek 02DB Sk tilde 02DC Sk hungarumlaut 02DD Sk rhotichookmod 02DE Sk crossmod 02DF Sk gammasupmod 02E0 Lm lsupmod 02E1 Lm ssupmod 02E2 Lm xsupmod 02E3 Lm glottalstopsupreversedmod 02E4 Lm tonebarextrahighmod 02E5 Sk tonebarhighmod 02E6 Sk tonebarmidmod 02E7 Sk tonebarlowmod 02E8 Sk tonebarextralowmod 02E9 Sk yintonemod 02EA Sk yangtonemod 02EB Sk voicingmod 02EC Lm unaspiratedmod 02ED Sk apostrophedblmod 02EE Lm arrowheaddownlowmod 02EF Sk arrowheaduplowmod 02F0 Sk arrowheadleftlowmod 02F1 Sk arrowheadrightlowmod 02F2 Sk ringlowmod 02F3 Sk gravemiddlemod 02F4 Sk gravedblmiddlemod 02F5 Sk acutedblmiddlemod 02F6 Sk tildelowmod 02F7 Sk colonraisedmod 02F8 Sk tonehighbeginmod 02F9 Sk tonehighendmod 02FA Sk tonelowbeginmod 02FB Sk tonelowendmod 02FC Sk shelfmod 02FD Sk shelfopenmod 02FE Sk arrowleftlowmod 02FF Sk # Combining Diacritical Marks gravecmb 0300 Mn acutecmb 0301 Mn circumflexcmb 0302 Mn tildecmb 0303 Mn macroncmb 0304 Mn overlinecmb 0305 Mn brevecmb 0306 Mn dotaccentcmb 0307 Mn dieresiscmb 0308 Mn hookabovecmb 0309 Mn ringabovecmb 030A Mn hungarumlautcmb 030B Mn caroncmb 030C Mn verticallineabovecmb 030D Mn dblverticallineabovecmb 030E Mn gravedoublecmb 030F Mn candrabinducmb 0310 Mn invertedbrevecmb 0311 Mn commaturnedabovecmb 0312 Mn turnedabovecmb 0313 Mn reversedcommaabovecmb 0314 Mn turnedcommaabovecmb 0315 Mn gravebelowcmb 0316 Mn acutebelowcmb 0317 Mn lefttackbelowcmb 0318 Mn righttackbelowcmb 0319 Mn leftangleabovecmb 031A Mn horncmb 031B Mn halfleftringbelowcmb 031C Mn uptackbelowcmb 031D Mn downtackbelowcmb 031E Mn plusbelowcmb 031F Mn minusbelowcmb 0320 Mn palatalizedhookbelowcmb 0321 Mn retroflexhookbelowcmb 0322 Mn dotbelowcmb 0323 Mn dieresisbelowcmb 0324 Mn ringbelowcmb 0325 Mn commaaccentbelowcmb 0326 Mn cedillacmb 0327 Mn ogonekcmb 0328 Mn verticallinebelowcmb 0329 Mn bridgebelowcmb 032A Mn dblarchinvertedbelowcmb 032B Mn caronbelowcmb 032C Mn circumflexbelowcmb 032D Mn belowbrevecmb 032E Mn invertedbelowbrevecmb 032F Mn tildebelowcmb 0330 Mn macronbelowcmb 0331 Mn lowlinecmb 0332 Mn lowlinedoublecmb 0333 Mn tildeoverlaycmb 0334 Mn overlaystrokeshortcmb 0335 Mn overlaystrokelongcmb 0336 Mn solidusshortoverlaycmb 0337 Mn soliduslongoverlaycmb 0338 Mn halfrightringbelowcmb 0339 Mn invertedbridgebelowcmb 033A Mn squarebelowcmb 033B Mn seagullbelowcmb 033C Mn xabovecmb 033D Mn tildeverticalcmb 033E Mn dbloverlinecmb 033F Mn gravetonecmb 0340 Mn acutetonecmb 0341 Mn perispomenicmb 0342 Mn koroniscmb 0343 Mn dialytikatonoscmb 0344 Mn iotasubcmb 0345 Mn bridgeabovecmb 0346 Mn equalbelowcmb 0347 Mn dblverticallinebelowcmb 0348 Mn leftanglebelowcmb 0349 Mn nottildeabovecmb 034A Mn homotheticabovecmb 034B Mn almostequalabovecmb 034C Mn arrowleftrightbelowcmb 034D Mn arrowupbelowcmb 034E Mn graphemejoinercmb 034F Mn arrowheadrightabovecmb 0350 Mn halfleftringabovecmb 0351 Mn fermatacmb 0352 Mn xbelowcmb 0353 Mn arrowheadleftbelowcmb 0354 Mn arrowheadrightbelowcmb 0355 Mn arrowheadrightarrowheadupbelowcmb 0356 Mn halfrightringabovecmb 0357 Mn dotrightabovecmb 0358 Mn asteriskbelowcmb 0359 Mn doubleringbelowcmb 035A Mn zigzagabovecmb 035B Mn doublebelowbrevecmb 035C Mn doublebrevecmb 035D Mn macrondoublecmb 035E Mn macrondoublebelowcmb 035F Mn tildedoublecmb 0360 Mn inverteddoublebrevecmb 0361 Mn arrowrightdoublebelowcmb 0362 Mn acmb 0363 Mn ecmb 0364 Mn icmb 0365 Mn ocmb 0366 Mn ucmb 0367 Mn ccmb 0368 Mn dcmb 0369 Mn hcmb 036A Mn mcmb 036B Mn rcmb 036C Mn tcmb 036D Mn vcmb 036E Mn xcmb 036F Mn # Greek and Coptic Heta 0370 Lu heta 0371 Ll Sampiarchaic 0372 Lu sampiarchaic 0373 Ll numeralsign 0374 Lm lownumeralsign 0375 Sk Digammapamphylian 0376 Lu digammapamphylian 0377 Ll iotasub 037A Lm sigmalunatereversedsymbol 037B Ll sigmalunatedottedsymbol 037C Ll sigmalunatedottedreversedsymbol 037D Ll gr:question 037E Po Yot 037F Lu tonos 0384 Sk dieresistonos 0385 Sk Alphatonos 0386 Lu anoteleia 0387 Po Epsilontonos 0388 Lu Etatonos 0389 Lu Iotatonos 038A Lu Omicrontonos 038C Lu Upsilontonos 038E Lu Omegatonos 038F Lu iotadieresistonos 0390 Ll Alpha 0391 Lu Beta 0392 Lu Gamma 0393 Lu Delta 0394 Lu Epsilon 0395 Lu Zeta 0396 Lu Eta 0397 Lu Theta 0398 Lu Iota 0399 Lu Kappa 039A Lu Lambda 039B Lu Mu 039C Lu Nu 039D Lu Xi 039E Lu Omicron 039F Lu Pi 03A0 Lu Rho 03A1 Lu Sigma 03A3 Lu Tau 03A4 Lu Upsilon 03A5 Lu Phi 03A6 Lu Chi 03A7 Lu Psi 03A8 Lu Omega 03A9 Lu Iotadieresis 03AA Lu Upsilondieresis 03AB Lu alphatonos 03AC Ll epsilontonos 03AD Ll etatonos 03AE Ll iotatonos 03AF Ll upsilondieresistonos 03B0 Ll alpha 03B1 Ll beta 03B2 Ll gamma 03B3 Ll delta 03B4 Ll epsilon 03B5 Ll zeta 03B6 Ll eta 03B7 Ll theta 03B8 Ll iota 03B9 Ll kappa 03BA Ll lambda 03BB Ll mu 03BC Ll nu 03BD Ll xi 03BE Ll omicron 03BF Ll pi 03C0 Ll rho 03C1 Ll finalsigma 03C2 Ll sigma 03C3 Ll tau 03C4 Ll upsilon 03C5 Ll phi 03C6 Ll chi 03C7 Ll psi 03C8 Ll omega 03C9 Ll iotadieresis 03CA Ll upsilondieresis 03CB Ll omicrontonos 03CC Ll upsilontonos 03CD Ll omegatonos 03CE Ll Kaisymbol 03CF Lu betasymbol 03D0 Ll theta.math 03D1 Ll Upsilonhooksymbol 03D2 Lu Upsilonacutehooksymbol 03D3 Lu Upsilonadieresishooksymbol 03D4 Lu phi.math 03D5 Ll pi.math 03D6 Ll kaisymbol 03D7 Ll Koppaarchaic 03D8 Lu koppaarchaic 03D9 Ll Stigma 03DA Lu stigma 03DB Ll Digamma 03DC Lu digamma 03DD Ll Koppa 03DE Lu koppa 03DF Ll Sampi 03E0 Lu sampi 03E1 Ll Sheicoptic 03E2 Lu sheicoptic 03E3 Ll Feicoptic 03E4 Lu feicoptic 03E5 Ll Kheicoptic 03E6 Lu kheicoptic 03E7 Ll Horicoptic 03E8 Lu horicoptic 03E9 Ll Gangiacoptic 03EA Lu gangiacoptic 03EB Ll Shimacoptic 03EC Lu shimacoptic 03ED Ll Deicoptic 03EE Lu deicoptic 03EF Ll kappa.math 03F0 Ll rhosymbol 03F1 Ll sigmalunatesymbol 03F2 Ll yot 03F3 Ll Thetasymbol 03F4 Lu epsilonlunatesymbol 03F5 Ll epsilonreversedlunatesymbol 03F6 Sm Sho 03F7 Lu sho 03F8 Ll Sigmalunatesymbol 03F9 Lu San 03FA Lu san 03FB Ll rhostrokesymbol 03FC Ll Sigmareversedlunatesymbol 03FD Lu Sigmalunatesymboldotted 03FE Lu Reverseddottedsigmalunatesymbol 03FF Lu # Cyrillic Iegravecyr 0400 Lu Iocyr 0401 Lu Djecyr 0402 Lu Gjecyr 0403 Lu Eukrcyr 0404 Lu Dzecyr 0405 Lu Iukrcyr 0406 Lu Yukrcyr 0407 Lu Jecyr 0408 Lu Ljecyr 0409 Lu Njecyr 040A Lu Tshecyr 040B Lu Kjecyr 040C Lu Igravecyr 040D Lu Ushortcyr 040E Lu Dzhecyr 040F Lu Acyr 0410 Lu Becyr 0411 Lu Vecyr 0412 Lu Gecyr 0413 Lu Decyr 0414 Lu Iecyr 0415 Lu Zhecyr 0416 Lu Zecyr 0417 Lu Icyr 0418 Lu Ishortcyr 0419 Lu Kacyr 041A Lu Elcyr 041B Lu Emcyr 041C Lu Encyr 041D Lu Ocyr 041E Lu Pecyr 041F Lu Ercyr 0420 Lu Escyr 0421 Lu Tecyr 0422 Lu Ucyr 0423 Lu Efcyr 0424 Lu Hacyr 0425 Lu Tsecyr 0426 Lu Checyr 0427 Lu Shacyr 0428 Lu Shchacyr 0429 Lu Hardcyr 042A Lu Ylongcyr 042B Lu Softcyr 042C Lu Ereversedcyr 042D Lu Yucyr 042E Lu Yacyr 042F Lu acyr 0430 Ll becyr 0431 Ll vecyr 0432 Ll gecyr 0433 Ll decyr 0434 Ll iecyr 0435 Ll zhecyr 0436 Ll zecyr 0437 Ll icyr 0438 Ll ishortcyr 0439 Ll kacyr 043A Ll elcyr 043B Ll emcyr 043C Ll encyr 043D Ll ocyr 043E Ll pecyr 043F Ll ercyr 0440 Ll escyr 0441 Ll tecyr 0442 Ll ucyr 0443 Ll efcyr 0444 Ll hacyr 0445 Ll tsecyr 0446 Ll checyr 0447 Ll shacyr 0448 Ll shchacyr 0449 Ll hardcyr 044A Ll ylongcyr 044B Ll softcyr 044C Ll ereversedcyr 044D Ll yucyr 044E Ll yacyr 044F Ll iegravecyr 0450 Ll iocyr 0451 Ll djecyr 0452 Ll gjecyr 0453 Ll eukrcyr 0454 Ll dzecyr 0455 Ll iukrcyr 0456 Ll yukrcyr 0457 Ll jecyr 0458 Ll ljecyr 0459 Ll njecyr 045A Ll tshecyr 045B Ll kjecyr 045C Ll igravecyr 045D Ll ushortcyr 045E Ll dzhecyr 045F Ll Omegacyr 0460 Lu omegacyr 0461 Ll Yatcyr 0462 Lu yatcyr 0463 Ll Eiotifiedcyr 0464 Lu eiotifiedcyr 0465 Ll Yuslittlecyr 0466 Lu yuslittlecyr 0467 Ll Yuslittleiotifiedcyr 0468 Lu yuslittleiotifiedcyr 0469 Ll Yusbigcyr 046A Lu yusbigcyr 046B Ll Yusbigiotifiedcyr 046C Lu yusbigiotifiedcyr 046D Ll Ksicyr 046E Lu ksicyr 046F Ll Psicyr 0470 Lu psicyr 0471 Ll Fitacyr 0472 Lu fitacyr 0473 Ll Izhitsacyr 0474 Lu izhitsacyr 0475 Ll Izhitsagravedblcyr 0476 Lu izhitsagravedblcyr 0477 Ll Ukcyr 0478 Lu ukcyr 0479 Ll Omegaroundcyr 047A Lu omegaroundcyr 047B Ll Omegatitlocyr 047C Lu omegatitlocyr 047D Ll Otcyr 047E Lu otcyr 047F Ll Koppacyr 0480 Lu koppacyr 0481 Ll thousandscyr 0482 So titlocmbcyr 0483 Mn palatcmbcyr 0484 Mn dasiacmbcyr 0485 Mn psilicmbcyr 0486 Mn pokrytiecmbcyr 0487 Mn hundredthousandscmbcyr 0488 Me millionscmbcyr 0489 Me Ishortsharptailcyr 048A Lu ishortsharptailcyr 048B Ll Semisoftcyr 048C Lu semisoftcyr 048D Ll Ertickcyr 048E Lu ertickcyr 048F Ll Geupcyr 0490 Lu geupcyr 0491 Ll Gestrokecyr 0492 Lu gestrokecyr 0493 Ll Gehookcyr 0494 Lu gehookcyr 0495 Ll Zhetailcyr 0496 Lu zhetailcyr 0497 Ll Zetailcyr 0498 Lu zetailcyr 0499 Ll Katailcyr 049A Lu katailcyr 049B Ll Kaverticalstrokecyr 049C Lu kaverticalstrokecyr 049D Ll Kastrokecyr 049E Lu kastrokecyr 049F Ll Kabashkcyr 04A0 Lu kabashkcyr 04A1 Ll Entailcyr 04A2 Lu entailcyr 04A3 Ll Engecyr 04A4 Lu engecyr 04A5 Ll Pehookcyr 04A6 Lu pehookcyr 04A7 Ll Haabkhcyr 04A8 Lu haabkhcyr 04A9 Ll Estailcyr 04AA Lu estailcyr 04AB Ll Tetailcyr 04AC Lu tetailcyr 04AD Ll Ustraightcyr 04AE Lu ustraightcyr 04AF Ll Ustraightstrokecyr 04B0 Lu ustraightstrokecyr 04B1 Ll Xatailcyr 04B2 Lu xatailcyr 04B3 Ll Tetsecyr 04B4 Lu tetsecyr 04B5 Ll Chetailcyr 04B6 Lu chetailcyr 04B7 Ll Chevertcyr 04B8 Lu chevertcyr 04B9 Ll Shhacyr 04BA Lu shhacyr 04BB Ll Cheabkhcyr 04BC Lu cheabkhcyr 04BD Ll Cheabkhtailcyr 04BE Lu cheabkhtailcyr 04BF Ll Palochkacyr 04C0 Lu Zhebrevecyr 04C1 Lu zhebrevecyr 04C2 Ll Kahookcyr 04C3 Lu kahookcyr 04C4 Ll Elsharptailcyr 04C5 Lu elsharptailcyr 04C6 Ll Enhookcyr 04C7 Lu enhookcyr 04C8 Ll Ensharptailcyr 04C9 Lu ensharptailcyr 04CA Ll Chekhakascyr 04CB Lu chekhakascyr 04CC Ll Emsharptailcyr 04CD Lu emsharptailcyr 04CE Ll palochkacyr 04CF Ll Abrevecyr 04D0 Lu abrevecyr 04D1 Ll Adieresiscyr 04D2 Lu adieresiscyr 04D3 Ll Aiecyr 04D4 Lu aiecyr 04D5 Ll Iebrevecyr 04D6 Lu iebrevecyr 04D7 Ll Schwacyr 04D8 Lu schwacyr 04D9 Ll Schwadieresiscyr 04DA Lu schwadieresiscyr 04DB Ll Zhedieresiscyr 04DC Lu zhedieresiscyr 04DD Ll Zedieresiscyr 04DE Lu zedieresiscyr 04DF Ll Dzeabkhcyr 04E0 Lu dzeabkhcyr 04E1 Ll Imacroncyr 04E2 Lu imacroncyr 04E3 Ll Idieresiscyr 04E4 Lu idieresiscyr 04E5 Ll Odieresiscyr 04E6 Lu odieresiscyr 04E7 Ll Obarcyr 04E8 Lu obarcyr 04E9 Ll Obardieresiscyr 04EA Lu obardieresiscyr 04EB Ll Ereverseddieresiscyr 04EC Lu ereverseddieresiscyr 04ED Ll Umacroncyr 04EE Lu umacroncyr 04EF Ll Udieresiscyr 04F0 Lu udieresiscyr 04F1 Ll Uacutedblcyr 04F2 Lu uacutedblcyr 04F3 Ll Chedieresiscyr 04F4 Lu chedieresiscyr 04F5 Ll Getailcyr 04F6 Lu getailcyr 04F7 Ll Ylongdieresiscyr 04F8 Lu ylongdieresiscyr 04F9 Ll Gehookstrokecyr 04FA Lu gehookstrokecyr 04FB Ll Hahookcyr 04FC Lu hahookcyr 04FD Ll Hastrokecyr 04FE Lu hastrokecyr 04FF Ll # Cyrillic Supplement Dekomicyr 0500 Lu dekomicyr 0501 Ll Djekomicyr 0502 Lu djekomicyr 0503 Ll Zjekomicyr 0504 Lu zjekomicyr 0505 Ll Dzjekomicyr 0506 Lu dzjekomicyr 0507 Ll Ljekomicyr 0508 Lu ljekomicyr 0509 Ll Njekomicyr 050A Lu njekomicyr 050B Ll Sjekomicyr 050C Lu sjekomicyr 050D Ll Tjekomicyr 050E Lu tjekomicyr 050F Ll Reversedzecyr 0510 Lu reversedzecyr 0511 Ll Elhookcyr 0512 Lu elhookcyr 0513 Ll Lhacyr 0514 Lu lhacyr 0515 Ll Rhacyr 0516 Lu rhacyr 0517 Ll Yaecyr 0518 Lu yaecyr 0519 Ll Qacyr 051A Lu qacyr 051B Ll Wecyr 051C Lu wecyr 051D Ll Kaaleutcyr 051E Lu kaaleutcyr 051F Ll Elmiddlehookcyr 0520 Lu elmiddlehookcyr 0521 Ll Enmiddlehookcyr 0522 Lu enmiddlehookcyr 0523 Ll Petailcyr 0524 Lu petailcyr 0525 Ll Shhatailcyr 0526 Lu shhatailcyr 0527 Ll Enhookleftcyr 0528 Lu enhookleftcyr 0529 Ll Dzzhecyr 052A Lu dzzhecyr 052B Ll Dchecyr 052C Lu dchecyr 052D Ll Eltailcyr 052E Lu eltailcyr 052F Ll # Armenian Aybarmn 0531 Lu Benarmn 0532 Lu Gimarmn 0533 Lu Daarmn 0534 Lu Echarmn 0535 Lu Zaarmn 0536 Lu Eharmn 0537 Lu Etarmn 0538 Lu Toarmn 0539 Lu Zhearmn 053A Lu Iniarmn 053B Lu Liwnarmn 053C Lu Xeharmn 053D Lu Caarmn 053E Lu Kenarmn 053F Lu Hoarmn 0540 Lu Jaarmn 0541 Lu Ghadarmn 0542 Lu Cheharmn 0543 Lu Menarmn 0544 Lu Yiarmn 0545 Lu Nowarmn 0546 Lu Shaarmn 0547 Lu Voarmn 0548 Lu Chaarmn 0549 Lu Peharmn 054A Lu Jheharmn 054B Lu Raarmn 054C Lu Seharmn 054D Lu Vewarmn 054E Lu Tiwnarmn 054F Lu Reharmn 0550 Lu Coarmn 0551 Lu Yiwnarmn 0552 Lu Piwrarmn 0553 Lu Keharmn 0554 Lu Oharmn 0555 Lu Feharmn 0556 Lu ringhalfleftarmn 0559 Lm apostrophearmn 055A Po emphasismarkarmn 055B Po exclamarmn 055C Po commaarmn 055D Po questionarmn 055E Po abbreviationmarkarmn 055F Po turnedaybarmn 0560 Ll aybarmn 0561 Ll benarmn 0562 Ll gimarmn 0563 Ll daarmn 0564 Ll echarmn 0565 Ll zaarmn 0566 Ll eharmn 0567 Ll etarmn 0568 Ll toarmn 0569 Ll zhearmn 056A Ll iniarmn 056B Ll liwnarmn 056C Ll xeharmn 056D Ll caarmn 056E Ll kenarmn 056F Ll hoarmn 0570 Ll jaarmn 0571 Ll ghadarmn 0572 Ll cheharmn 0573 Ll menarmn 0574 Ll yiarmn 0575 Ll nowarmn 0576 Ll shaarmn 0577 Ll voarmn 0578 Ll chaarmn 0579 Ll peharmn 057A Ll jheharmn 057B Ll raarmn 057C Ll seharmn 057D Ll vewarmn 057E Ll tiwnarmn 057F Ll reharmn 0580 Ll coarmn 0581 Ll yiwnarmn 0582 Ll piwrarmn 0583 Ll keharmn 0584 Ll oharmn 0585 Ll feharmn 0586 Ll ech_yiwnarmn 0587 Ll yiwithstrokearmn 0588 Ll periodarmn 0589 Po hyphenarmn 058A Pd rightfacingeternitysignarmn 058D So leftfacingeternitysignarmn 058E So dramsignarmn 058F Sc # Hebrew etnahta:hb 0591 Mn segolta:hb 0592 Mn shalshelet:hb 0593 Mn zaqefQatan:hb 0594 Mn zaqefGadol:hb 0595 Mn tifcha:hb 0596 Mn revia:hb 0597 Mn zarqa:hb 0598 Mn pashta:hb 0599 Mn yetiv:hb 059A Mn tevir:hb 059B Mn azla:hb 059C Mn gereshMuqdam:hb 059D Mn SheneGerishin:hb 059E Mn qarneFarah:hb 059F Mn telishaGedolah:hb 05A0 Mn pazer:hb 05A1 Mn atnachHafukh:hb 05A2 Mn munach:hb 05A3 Mn mahpach:hb 05A4 Mn mercha:hb 05A5 Mn merchaKefulah:hb 05A6 Mn darga:hb 05A7 Mn qadma:hb 05A8 Mn telishaQetannah:hb 05A9 Mn yerachBenYomo:hb 05AA Mn ole:hb 05AB Mn iluy:hb 05AC Mn dehi:hb 05AD Mn tsinnorit:hb 05AE Mn masoraCircle:hb 05AF Mn sheva:hb 05B0 Mn hatafSegol:hb 05B1 Mn hatafPatah:hb 05B2 Mn hatafQamats:hb 05B3 Mn hiriq:hb 05B4 Mn tsere:hb 05B5 Mn segol:hb 05B6 Mn patah:hb 05B7 Mn qamats:hb 05B8 Mn holam:hb 05B9 Mn holamHaser:hb 05BA Mn qubuts:hb 05BB Mn dagesh:hb 05BC Mn meteg:hb 05BD Mn maqaf:hb 05BE Pd rafe:hb 05BF Mn paseq:hb 05C0 Po shinDot:hb 05C1 Mn sinDot:hb 05C2 Mn sofPasuq:hb 05C3 Po dotupper:hb 05C4 Mn dotlower:hb 05C5 Mn nunHafukha:hb 05C6 Po qamatsQatan:hb 05C7 Mn alef:hb 05D0 Lo bet:hb 05D1 Lo gimel:hb 05D2 Lo dalet:hb 05D3 Lo he:hb 05D4 Lo vav:hb 05D5 Lo zayin:hb 05D6 Lo het:hb 05D7 Lo tet:hb 05D8 Lo yod:hb 05D9 Lo finalkaf:hb 05DA Lo kaf:hb 05DB Lo lamed:hb 05DC Lo finalmem:hb 05DD Lo mem:hb 05DE Lo finalnun:hb 05DF Lo nun:hb 05E0 Lo samekh:hb 05E1 Lo ayin:hb 05E2 Lo finalpe:hb 05E3 Lo pe:hb 05E4 Lo finaltsadi:hb 05E5 Lo tsadi:hb 05E6 Lo qof:hb 05E7 Lo resh:hb 05E8 Lo shin:hb 05E9 Lo tav:hb 05EA Lo yodtriangle:hb 05EF Lo vav_vav:hb 05F0 Lo vav_yod:hb 05F1 Lo yod_yod:hb 05F2 Lo geresh:hb 05F3 Po gershayim:hb 05F4 Po # Arabic arnumbersign 0600 Cf Sanah 0601 Cf footnote 0602 Cf Safha 0603 Cf samvat 0604 Cf numbermarkabove 0605 Cf arcuberoot 0606 Sm arfourthroot 0607 Sm ray 0608 Sm permille 0609 Po arperthousand 060A Po afghani 060B Sc arcomma 060C Po dateseparator 060D Po poeticverse 060E So misra 060F So HonSAW 0610 Mn HonAA 0611 Mn HonRA 0612 Mn aHonRAA 0613 Mn takhallus 0614 Mn tahabove 0615 Mn alefLamYehabove 0616 Mn zainabove 0617 Mn fathasmall 0618 Mn dammasmall 0619 Mn kasrasmall 061A Mn arsemicolon 061B Po mark 061C Cf tripledot 061E Po arquestion 061F Po kashmiriyeh 0620 Lo hamza 0621 Lo alefmadda 0622 Lo alefhamza 0623 Lo wawhamza 0624 Lo alefhamzabelow 0625 Lo yehhamza 0626 Lo alef 0627 Lo beh 0628 Lo tehmarbuta 0629 Lo teh 062A Lo theh 062B Lo jeem 062C Lo hah 062D Lo khah 062E Lo dal 062F Lo thal 0630 Lo reh 0631 Lo zain 0632 Lo seen 0633 Lo sheen 0634 Lo sad 0635 Lo dad 0636 Lo tah 0637 Lo zah 0638 Lo arain 0639 Lo ghain 063A Lo kehehtwodotsabove 063B Lo kehehthreedotsbelow 063C Lo yehfarsiinvertedV 063D Lo yehfarsitwodotsabove 063E Lo yehfarsithreedotsabove 063F Lo kashida 0640 Lm feh 0641 Lo qaf 0642 Lo kaf 0643 Lo lam 0644 Lo meem 0645 Lo noon 0646 Lo heh 0647 Lo waw 0648 Lo alefmaksura 0649 Lo yeh 064A Lo fathatan 064B Mn dammatan 064C Mn kasratan 064D Mn fatha 064E Mn damma 064F Mn kasra 0650 Mn shadda 0651 Mn sukun 0652 Mn madda 0653 Mn hamzaabove 0654 Mn hamzabelow 0655 Mn subscriptalef 0656 Mn inverteddamma 0657 Mn marknoonghunna 0658 Mn zwarakay 0659 Mn vowelVabove 065A Mn vowelinvertedVabove 065B Mn voweldotbelow 065C Mn dammareversed 065D Mn fathatwodotsdots 065E Mn wavyhamzabelow 065F Mn arzero 0660 Nd arone 0661 Nd artwo 0662 Nd arthree 0663 Nd arfour 0664 Nd arfive 0665 Nd arsix 0666 Nd arseven 0667 Nd areight 0668 Nd arnine 0669 Nd arpercent 066A Po ardecimalseparator 066B Po thousandsseparator 066C Po fivepointedstar 066D Po dotlessbeh 066E Lo dotlessqaf 066F Lo alefabove 0670 Mn alefwasla 0671 Lo alefwavyhamza 0672 Lo alefwavyhamzabelow 0673 Lo highhamza 0674 Lo alefhighhamza 0675 Lo wawhighhamza 0676 Lo uhamza 0677 Lo yehhighhamza 0678 Lo tteh 0679 Lo tteheh 067A Lo beeh 067B Lo tehring 067C Lo tehdownthreedotsabove 067D Lo peh 067E Lo teheh 067F Lo beheh 0680 Lo hahhamza 0681 Lo hahtwodotsvertical 0682 Lo nyeh 0683 Lo dyeh 0684 Lo hahthreedotsabove 0685 Lo tcheh 0686 Lo tcheheh 0687 Lo ddal 0688 Lo dalring 0689 Lo daldotbelow 068A Lo daldotbelowtahsmall 068B Lo dahal 068C Lo ddahal 068D Lo dul 068E Lo daldownthreedotsabove 068F Lo dalfourdotsabove 0690 Lo rreh 0691 Lo rehVabove 0692 Lo rehring 0693 Lo rehdotbelow 0694 Lo rehVbelow 0695 Lo rehdotbelowdotabove 0696 Lo rehtwodotsabove 0697 Lo jeh 0698 Lo rehfourdotsabove 0699 Lo seendotbelowdotabove 069A Lo seenthreedotsbelow 069B Lo seenthreedotsbelowthreedotsabove 069C Lo sadtwodotsbelow 069D Lo sadthreedotsabove 069E Lo tahthreedotsabove 069F Lo ainthreedotsabove 06A0 Lo dotlessfeh 06A1 Lo fehdotbelowright 06A2 Lo fehdotbelow 06A3 Lo veh 06A4 Lo fehthreedotsbelow 06A5 Lo peheh 06A6 Lo qafdotabove 06A7 Lo qafthreedotsabove 06A8 Lo keheh 06A9 Lo kafswash 06AA Lo kafring 06AB Lo kafdotabove 06AC Lo ng 06AD Lo kafthreedotsbelow 06AE Lo gaf 06AF Lo gafring 06B0 Lo ngoeh 06B1 Lo gaftwodotsbelow 06B2 Lo gueh 06B3 Lo gafthreedotsabove 06B4 Lo lamVabove 06B5 Lo lamdotabove 06B6 Lo lamthreedotsabove 06B7 Lo lamthreedotsbelow 06B8 Lo noondotbelow 06B9 Lo noonghunna 06BA Lo rnoon 06BB Lo noonring 06BC Lo noonthreedotsabove 06BD Lo hehdoachashmee 06BE Lo tchehdotabove 06BF Lo hehyeh 06C0 Lo hehgoal 06C1 Lo hehgoalhamza 06C2 Lo tehmarbutagoal 06C3 Lo wawring 06C4 Lo oekirghiz 06C5 Lo aroe 06C6 Lo aru 06C7 Lo aryu 06C8 Lo yukirghiz 06C9 Lo wawtwodotsabove 06CA Lo ve 06CB Lo yehfarsi 06CC Lo yehtail 06CD Lo yehVabove 06CE Lo wawdotabove 06CF Lo are 06D0 Lo yehthreedotsbelow 06D1 Lo yehbarree 06D2 Lo yehbarreehamza 06D3 Lo periodurdu 06D4 Po arae 06D5 Lo sad_lam_alefmaksuraabove 06D6 Mn qaf_lam_alefmaksuraabove 06D7 Mn meemabove.init 06D8 Mn lamalefabove 06D9 Mn jeemabove 06DA Mn threedotsaboveabove 06DB Mn seenabove 06DC Mn Ayahend 06DD Cf RubElHizbstart 06DE So roundedzeroabove 06DF Mn zerosquareabove 06E0 Mn dotlesskhahabove 06E1 Mn meemabove 06E2 Mn seenlow 06E3 Mn maddaabove 06E4 Mn wawsmall 06E5 Lm yehsmall 06E6 Lm yehabove 06E7 Mn noonabove 06E8 Mn Sajdah 06E9 So stopbelow 06EA Mn stopabove 06EB Mn filledstopabove 06EC Mn meembelow 06ED Mn dalinvertedV 06EE Lo rehinvertedV 06EF Lo arzero 06F0 Nd arone 06F1 Nd artwo 06F2 Nd arthree 06F3 Nd arfour 06F4 Nd arfive 06F5 Nd arsix 06F6 Nd arseven 06F7 Nd areight 06F8 Nd arnine 06F9 Nd sheendotbelow 06FA Lo daddotbelow 06FB Lo ghaindotbelow 06FC Lo ampersandSindhi 06FD So menpostSindhi 06FE So hehinvertedV 06FF Lo # Arabic Supplement behThreeDotsHorizontallyBelow 0750 Lo behDotBelowThreeDotsAbove 0751 Lo behThreeDotsUpBelow 0752 Lo behThreeDotsUpBelowTwoDotsAbove 0753 Lo behTwoDotsBelowDotAbove 0754 Lo behInvertedSmallVBelow 0755 Lo behSmallV 0756 Lo hahTwoDotsAbove 0757 Lo hahThreeDotsUpBelow 0758 Lo dalTwoDotsVerticallyBelowSmallTah 0759 Lo dalInvertedSmallVBelow 075A Lo rehStroke 075B Lo seenFourDotsAbove 075C Lo ainTwoDotsAbove 075D Lo ainThreeDotsDownAbove 075E Lo ainTwoDotsVerticallyAbove 075F Lo fehTwoDotsBelow 0760 Lo fehThreeDotsUpBelow 0761 Lo kehehDotAbove 0762 Lo kehehThreeDotsAbove 0763 Lo kehehThreeDotsUpBelow 0764 Lo meemDotAbove 0765 Lo meemDotBelow 0766 Lo noonTwoDotsBelow 0767 Lo noonSmallTah 0768 Lo noonSmallV 0769 Lo lamBar 076A Lo rehTwoDotsVerticallyAbove 076B Lo rehHamzaAbove 076C Lo seenTwoDotsVerticallyAbove 076D Lo hahSmallTahBelow 076E Lo hahSmallTahTwoDots 076F Lo seenSmallTahTwoDots 0770 Lo rehSmallTahTwoDots 0771 Lo hahSmallTahAbove 0772 Lo alefDigitTwoAbove 0773 Lo alefDigitThreeAbove 0774 Lo farsiYehDigitTwoAbove 0775 Lo farsiYehDigitThreeAbove 0776 Lo farsiYehDigitFourBelow 0777 Lo wawDigitTwoAbove 0778 Lo wawDigitThreeAbove 0779 Lo yehBarreeDigitTwoAbove 077A Lo yehBarreeDigitThreeAbove 077B Lo hahDigitFourBelow 077C Lo seenDigitFourAbove 077D Lo seenInvertedV 077E Lo kafTwoDotsAbove 077F Lo # Devanagari deva:candrabinduinverted 0900 Mn deva:candrabindu 0901 Mn deva:anusvara 0902 Mn deva:visarga 0903 Mc deva:ashort 0904 Lo deva:a 0905 Lo deva:aa 0906 Lo deva:i 0907 Lo deva:ii 0908 Lo deva:u 0909 Lo deva:uu 090A Lo deva:vocalicr 090B Lo deva:vocalicl 090C Lo deva:ecandra 090D Lo deva:eshort 090E Lo deva:e 090F Lo deva:ai 0910 Lo deva:ocandra 0911 Lo deva:oshort 0912 Lo deva:o 0913 Lo deva:au 0914 Lo deva:ka 0915 Lo deva:kha 0916 Lo deva:ga 0917 Lo deva:gha 0918 Lo deva:nga 0919 Lo deva:ca 091A Lo deva:cha 091B Lo deva:ja 091C Lo deva:jha 091D Lo deva:nya 091E Lo deva:tta 091F Lo deva:ttha 0920 Lo deva:dda 0921 Lo deva:ddha 0922 Lo deva:nna 0923 Lo deva:ta 0924 Lo deva:tha 0925 Lo deva:da 0926 Lo deva:dha 0927 Lo deva:na 0928 Lo deva:nnna 0929 Lo deva:pa 092A Lo deva:pha 092B Lo deva:ba 092C Lo deva:bha 092D Lo deva:ma 092E Lo deva:ya 092F Lo deva:ra 0930 Lo deva:rra 0931 Lo deva:la 0932 Lo deva:lla 0933 Lo deva:llla 0934 Lo deva:va 0935 Lo deva:sha 0936 Lo deva:ssa 0937 Lo deva:sa 0938 Lo deva:ha 0939 Lo deva:oesign 093A Mn deva:ooesign 093B Mc deva:nukta 093C Mn deva:avagraha 093D Lo deva:aasign 093E Mc deva:isign 093F Mc deva:iisign 0940 Mc deva:usign 0941 Mn deva:uusign 0942 Mn deva:vocalicrsign 0943 Mn deva:vocalicrrsign 0944 Mn deva:esigncandra 0945 Mn deva:esignshort 0946 Mn deva:esign 0947 Mn deva:aisign 0948 Mn deva:osigncandra 0949 Mc deva:osignshort 094A Mc deva:osign 094B Mc deva:ausign 094C Mc deva:virama 094D Mn deva:esignprishthamatra 094E Mc deva:awsign 094F Mc deva:om 0950 Lo deva:udatta 0951 Mn deva:anudatta 0952 Mn deva:grave 0953 Mn deva:acute 0954 Mn deva:esigncandralong 0955 Mn deva:uesign 0956 Mn deva:uuesign 0957 Mn deva:qa 0958 Lo deva:khha 0959 Lo deva:ghha 095A Lo deva:za 095B Lo deva:dddha 095C Lo deva:rha 095D Lo deva:fa 095E Lo deva:yya 095F Lo deva:vocalicrr 0960 Lo deva:vocalicll 0961 Lo deva:vocaliclsign 0962 Mn deva:vocalicllsign 0963 Mn deva:danda 0964 Po deva:dbldanda 0965 Po deva:zero 0966 Nd deva:one 0967 Nd deva:two 0968 Nd deva:three 0969 Nd deva:four 096A Nd deva:five 096B Nd deva:six 096C Nd deva:seven 096D Nd deva:eight 096E Nd deva:nine 096F Nd deva:abbreviation 0970 Po deva:dothigh 0971 Lm deva:acandra 0972 Lo deva:oe 0973 Lo deva:ooe 0974 Lo deva:aw 0975 Lo deva:ue 0976 Lo deva:uue 0977 Lo deva:marwaridda 0978 Lo deva:zha 0979 Lo deva:yaheavy 097A Lo deva:gga 097B Lo deva:jja 097C Lo deva:glottalstop 097D Lo deva:ddda 097E Lo deva:bba 097F Lo # Bengali beng:anji 0980 Lo beng:candrabindu 0981 Mn beng:anusvara 0982 Mc beng:visarga 0983 Mc beng:a 0985 Lo beng:aa 0986 Lo beng:i 0987 Lo beng:ii 0988 Lo beng:u 0989 Lo beng:uu 098A Lo beng:vocalicr 098B Lo beng:vocalicl 098C Lo beng:e 098F Lo beng:ai 0990 Lo beng:o 0993 Lo beng:au 0994 Lo beng:ka 0995 Lo beng:kha 0996 Lo beng:ga 0997 Lo beng:gha 0998 Lo beng:nga 0999 Lo beng:ca 099A Lo beng:cha 099B Lo beng:ja 099C Lo beng:jha 099D Lo beng:nya 099E Lo beng:tta 099F Lo beng:ttha 09A0 Lo beng:dda 09A1 Lo beng:ddha 09A2 Lo beng:nna 09A3 Lo beng:ta 09A4 Lo beng:tha 09A5 Lo beng:da 09A6 Lo beng:dha 09A7 Lo beng:na 09A8 Lo beng:pa 09AA Lo beng:pha 09AB Lo beng:ba 09AC Lo beng:bha 09AD Lo beng:ma 09AE Lo beng:ya 09AF Lo beng:ra 09B0 Lo beng:la 09B2 Lo beng:sha 09B6 Lo beng:ssa 09B7 Lo beng:sa 09B8 Lo beng:ha 09B9 Lo beng:nukta 09BC Mn beng:avagraha 09BD Lo beng:aasign 09BE Mc beng:isign 09BF Mc beng:iisign 09C0 Mc beng:usign 09C1 Mn beng:uusign 09C2 Mn beng:vocalicrsign 09C3 Mn beng:vocalicrrsign 09C4 Mn beng:esign 09C7 Mc beng:aisign 09C8 Mc beng:osign 09CB Mc beng:ausign 09CC Mc beng:virama 09CD Mn beng:khandata 09CE Lo beng:aulengthmark 09D7 Mc beng:rra 09DC Lo beng:rha 09DD Lo beng:yya 09DF Lo beng:vocalicrr 09E0 Lo beng:vocalicll 09E1 Lo beng:vocaliclsign 09E2 Mn beng:vocalicllsign 09E3 Mn beng:zero 09E6 Nd beng:one 09E7 Nd beng:two 09E8 Nd beng:three 09E9 Nd beng:four 09EA Nd beng:five 09EB Nd beng:six 09EC Nd beng:seven 09ED Nd beng:eight 09EE Nd beng:nine 09EF Nd beng:ramiddiagonal 09F0 Lo beng:ralowdiagonal 09F1 Lo beng:rupeemark 09F2 Sc beng:rupee 09F3 Sc beng:onecurrencynumerator 09F4 No beng:twocurrencynumerator 09F5 No beng:threecurrencynumerator 09F6 No beng:fourcurrencynumerator 09F7 No beng:currencyoneless 09F8 No beng:sixteencurrencydenominator 09F9 No beng:isshar 09FA So beng:gandamark 09FB Sc beng:vedicanusvara 09FC Lo beng:abbreviationsign 09FD Po beng:sandhimark 09FE Mn # Gurmukhi guru:adakbindisign 0A01 Mn guru:bindisign 0A02 Mn guru:visarga 0A03 Mc guru:a 0A05 Lo guru:aa 0A06 Lo guru:i 0A07 Lo guru:ii 0A08 Lo guru:u 0A09 Lo guru:uu 0A0A Lo guru:ee 0A0F Lo guru:ai 0A10 Lo guru:oo 0A13 Lo guru:au 0A14 Lo guru:ka 0A15 Lo guru:kha 0A16 Lo guru:ga 0A17 Lo guru:gha 0A18 Lo guru:nga 0A19 Lo guru:ca 0A1A Lo guru:cha 0A1B Lo guru:ja 0A1C Lo guru:jha 0A1D Lo guru:nya 0A1E Lo guru:tta 0A1F Lo guru:ttha 0A20 Lo guru:dda 0A21 Lo guru:ddha 0A22 Lo guru:nna 0A23 Lo guru:ta 0A24 Lo guru:tha 0A25 Lo guru:da 0A26 Lo guru:dha 0A27 Lo guru:na 0A28 Lo guru:pa 0A2A Lo guru:pha 0A2B Lo guru:ba 0A2C Lo guru:bha 0A2D Lo guru:ma 0A2E Lo guru:ya 0A2F Lo guru:ra 0A30 Lo guru:la 0A32 Lo guru:lla 0A33 Lo guru:va 0A35 Lo guru:sha 0A36 Lo guru:sa 0A38 Lo guru:ha 0A39 Lo guru:nukta 0A3C Mn guru:aasign 0A3E Mc guru:isign 0A3F Mc guru:iisign 0A40 Mc guru:usign 0A41 Mn guru:uusign 0A42 Mn guru:eesign 0A47 Mn guru:aisign 0A48 Mn guru:oosign 0A4B Mn guru:ausign 0A4C Mn guru:virama 0A4D Mn guru:udaatsign 0A51 Mn guru:khha 0A59 Lo guru:ghha 0A5A Lo guru:za 0A5B Lo guru:rra 0A5C Lo guru:fa 0A5E Lo guru:zero 0A66 Nd guru:one 0A67 Nd guru:two 0A68 Nd guru:three 0A69 Nd guru:four 0A6A Nd guru:five 0A6B Nd guru:six 0A6C Nd guru:seven 0A6D Nd guru:eight 0A6E Nd guru:nine 0A6F Nd guru:tippi 0A70 Mn guru:addak 0A71 Mn guru:iri 0A72 Lo guru:ura 0A73 Lo guru:ekonkar 0A74 Lo guru:yakashsign 0A75 Mn guru:abbreviationsign 0A76 Po # Gujarati gujr:candrabindu 0A81 Mn gujr:anusvara 0A82 Mn gujr:visarga 0A83 Mc gujr:a 0A85 Lo gujr:aa 0A86 Lo gujr:i 0A87 Lo gujr:ii 0A88 Lo gujr:u 0A89 Lo gujr:uu 0A8A Lo gujr:vocalicr 0A8B Lo gujr:vocalicl 0A8C Lo gujr:ecandra 0A8D Lo gujr:e 0A8F Lo gujr:ai 0A90 Lo gujr:ocandra 0A91 Lo gujr:o 0A93 Lo gujr:au 0A94 Lo gujr:ka 0A95 Lo gujr:kha 0A96 Lo gujr:ga 0A97 Lo gujr:gha 0A98 Lo gujr:nga 0A99 Lo gujr:ca 0A9A Lo gujr:cha 0A9B Lo gujr:ja 0A9C Lo gujr:jha 0A9D Lo gujr:nya 0A9E Lo gujr:tta 0A9F Lo gujr:ttha 0AA0 Lo gujr:dda 0AA1 Lo gujr:ddha 0AA2 Lo gujr:nna 0AA3 Lo gujr:ta 0AA4 Lo gujr:tha 0AA5 Lo gujr:da 0AA6 Lo gujr:dha 0AA7 Lo gujr:na 0AA8 Lo gujr:pa 0AAA Lo gujr:pha 0AAB Lo gujr:ba 0AAC Lo gujr:bha 0AAD Lo gujr:ma 0AAE Lo gujr:ya 0AAF Lo gujr:ra 0AB0 Lo gujr:la 0AB2 Lo gujr:lla 0AB3 Lo gujr:va 0AB5 Lo gujr:sha 0AB6 Lo gujr:ssa 0AB7 Lo gujr:sa 0AB8 Lo gujr:ha 0AB9 Lo gujr:nukta 0ABC Mn gujr:avagraha 0ABD Lo gujr:aasign 0ABE Mc gujr:isign 0ABF Mc gujr:iisign 0AC0 Mc gujr:usign 0AC1 Mn gujr:uusign 0AC2 Mn gujr:vocalicrsign 0AC3 Mn gujr:vocalicrrsign 0AC4 Mn gujr:esigncandra 0AC5 Mn gujr:esign 0AC7 Mn gujr:aisign 0AC8 Mn gujr:osigncandra 0AC9 Mc gujr:osign 0ACB Mc gujr:ausign 0ACC Mc gujr:virama 0ACD Mn gujr:om 0AD0 Lo gujr:vocalicrr 0AE0 Lo gujr:vocalicll 0AE1 Lo gujr:vocaliclsign 0AE2 Mn gujr:vocalicllsign 0AE3 Mn gujr:zero 0AE6 Nd gujr:one 0AE7 Nd gujr:two 0AE8 Nd gujr:three 0AE9 Nd gujr:four 0AEA Nd gujr:five 0AEB Nd gujr:six 0AEC Nd gujr:seven 0AED Nd gujr:eight 0AEE Nd gujr:nine 0AEF Nd gujr:abbreviation 0AF0 Po gujr:rupee 0AF1 Sc gujr:zha 0AF9 Lo gujr:sukun 0AFA Mn gujr:shadda 0AFB Mn gujr:maddah 0AFC Mn gujr:threedotnuktaabove 0AFD Mn gujr:circlenuktaabove 0AFE Mn gujr:twocirclenuktaabove 0AFF Mn # Oriya orya:candrabindu 0B01 Mn orya:anusvara 0B02 Mc orya:visarga 0B03 Mc orya:a 0B05 Lo orya:aa 0B06 Lo orya:i 0B07 Lo orya:ii 0B08 Lo orya:u 0B09 Lo orya:uu 0B0A Lo orya:vocalicr 0B0B Lo orya:vocalicl 0B0C Lo orya:e 0B0F Lo orya:ai 0B10 Lo orya:o 0B13 Lo orya:au 0B14 Lo orya:ka 0B15 Lo orya:kha 0B16 Lo orya:ga 0B17 Lo orya:gha 0B18 Lo orya:nga 0B19 Lo orya:ca 0B1A Lo orya:cha 0B1B Lo orya:ja 0B1C Lo orya:jha 0B1D Lo orya:nya 0B1E Lo orya:tta 0B1F Lo orya:ttha 0B20 Lo orya:dda 0B21 Lo orya:ddha 0B22 Lo orya:nna 0B23 Lo orya:ta 0B24 Lo orya:tha 0B25 Lo orya:da 0B26 Lo orya:dha 0B27 Lo orya:na 0B28 Lo orya:pa 0B2A Lo orya:pha 0B2B Lo orya:ba 0B2C Lo orya:bha 0B2D Lo orya:ma 0B2E Lo orya:ya 0B2F Lo orya:ra 0B30 Lo orya:la 0B32 Lo orya:lla 0B33 Lo orya:va 0B35 Lo orya:sha 0B36 Lo orya:ssa 0B37 Lo orya:sa 0B38 Lo orya:ha 0B39 Lo orya:nukta 0B3C Mn orya:avagraha 0B3D Lo orya:aasign 0B3E Mc orya:isign 0B3F Mn orya:iisign 0B40 Mc orya:usign 0B41 Mn orya:uusign 0B42 Mn orya:vocalicrsign 0B43 Mn orya:vocalicrrsign 0B44 Mn orya:esign 0B47 Mc orya:aisign 0B48 Mc orya:osign 0B4B Mc orya:ausign 0B4C Mc orya:virama 0B4D Mn orya:ailengthmark 0B56 Mn orya:aulengthmark 0B57 Mc orya:rra 0B5C Lo orya:rha 0B5D Lo orya:yya 0B5F Lo orya:vocalicrr 0B60 Lo orya:vocalicll 0B61 Lo orya:vocaliclsign 0B62 Mn orya:vocalicllsign 0B63 Mn orya:zero 0B66 Nd orya:one 0B67 Nd orya:two 0B68 Nd orya:three 0B69 Nd orya:four 0B6A Nd orya:five 0B6B Nd orya:six 0B6C Nd orya:seven 0B6D Nd orya:eight 0B6E Nd orya:nine 0B6F Nd orya:isshar 0B70 So orya:wa 0B71 Lo orya:onequarter 0B72 No orya:onehalf 0B73 No orya:threequarters 0B74 No orya:onesixteenth 0B75 No orya:oneeighth 0B76 No orya:threesixteenths 0B77 No # Tamil taml:anusvara 0B82 Mn taml:visarga 0B83 Lo taml:a 0B85 Lo taml:aa 0B86 Lo taml:i 0B87 Lo taml:ii 0B88 Lo taml:u 0B89 Lo taml:uu 0B8A Lo taml:e 0B8E Lo taml:ee 0B8F Lo taml:ai 0B90 Lo taml:o 0B92 Lo taml:oo 0B93 Lo taml:au 0B94 Lo taml:ka 0B95 Lo taml:nga 0B99 Lo taml:ca 0B9A Lo taml:ja 0B9C Lo taml:nya 0B9E Lo taml:tta 0B9F Lo taml:nna 0BA3 Lo taml:ta 0BA4 Lo taml:na 0BA8 Lo taml:nnna 0BA9 Lo taml:pa 0BAA Lo taml:ma 0BAE Lo taml:ya 0BAF Lo taml:ra 0BB0 Lo taml:rra 0BB1 Lo taml:la 0BB2 Lo taml:lla 0BB3 Lo taml:llla 0BB4 Lo taml:va 0BB5 Lo taml:sha 0BB6 Lo taml:ssa 0BB7 Lo taml:sa 0BB8 Lo taml:ha 0BB9 Lo taml:aasign 0BBE Mc taml:isign 0BBF Mc taml:iisign 0BC0 Mn taml:usign 0BC1 Mc taml:uusign 0BC2 Mc taml:esign 0BC6 Mc taml:eesign 0BC7 Mc taml:aisign 0BC8 Mc taml:osign 0BCA Mc taml:oosign 0BCB Mc taml:ausign 0BCC Mc taml:virama 0BCD Mn taml:om 0BD0 Lo taml:aulengthmark 0BD7 Mc taml:zero 0BE6 Nd taml:one 0BE7 Nd taml:two 0BE8 Nd taml:three 0BE9 Nd taml:four 0BEA Nd taml:five 0BEB Nd taml:six 0BEC Nd taml:seven 0BED Nd taml:eight 0BEE Nd taml:nine 0BEF Nd taml:ten 0BF0 No taml:onehundred 0BF1 No taml:onethousand 0BF2 No taml:daysign 0BF3 So taml:monthsign 0BF4 So taml:yearsign 0BF5 So taml:debitsign 0BF6 So taml:creditsign 0BF7 So taml:asabovesign 0BF8 So taml:rupee 0BF9 Sc taml:sign 0BFA So # Telugu telu:combiningcandrabinduabovesign 0C00 Mn telu:candrabindusign 0C01 Mc telu:anusvara 0C02 Mc telu:visarga 0C03 Mc telu:combininganusvaraabovesign 0C04 Mn telu:a 0C05 Lo telu:aa 0C06 Lo telu:i 0C07 Lo telu:ii 0C08 Lo telu:u 0C09 Lo telu:uu 0C0A Lo telu:vocalicr 0C0B Lo telu:vocalicl 0C0C Lo telu:e 0C0E Lo telu:ee 0C0F Lo telu:ai 0C10 Lo telu:o 0C12 Lo telu:oo 0C13 Lo telu:au 0C14 Lo telu:ka 0C15 Lo telu:kha 0C16 Lo telu:ga 0C17 Lo telu:gha 0C18 Lo telu:nga 0C19 Lo telu:ca 0C1A Lo telu:cha 0C1B Lo telu:ja 0C1C Lo telu:jha 0C1D Lo telu:nya 0C1E Lo telu:tta 0C1F Lo telu:ttha 0C20 Lo telu:dda 0C21 Lo telu:ddha 0C22 Lo telu:nna 0C23 Lo telu:ta 0C24 Lo telu:tha 0C25 Lo telu:da 0C26 Lo telu:dha 0C27 Lo telu:na 0C28 Lo telu:pa 0C2A Lo telu:pha 0C2B Lo telu:ba 0C2C Lo telu:bha 0C2D Lo telu:ma 0C2E Lo telu:ya 0C2F Lo telu:ra 0C30 Lo telu:rra 0C31 Lo telu:la 0C32 Lo telu:lla 0C33 Lo telu:llla 0C34 Lo telu:va 0C35 Lo telu:sha 0C36 Lo telu:ssa 0C37 Lo telu:sa 0C38 Lo telu:ha 0C39 Lo telu:avagraha 0C3D Lo telu:aasign 0C3E Mn telu:isign 0C3F Mn telu:iisign 0C40 Mn telu:usign 0C41 Mc telu:uusign 0C42 Mc telu:vocalicrsign 0C43 Mc telu:vocalicrrsign 0C44 Mc telu:esign 0C46 Mn telu:eesign 0C47 Mn telu:aisign 0C48 Mn telu:osign 0C4A Mn telu:oosign 0C4B Mn telu:ausign 0C4C Mn telu:virama 0C4D Mn telu:lengthmark 0C55 Mn telu:ailengthmark 0C56 Mn telu:tsa 0C58 Lo telu:dza 0C59 Lo telu:rrra 0C5A Lo telu:vocalicrr 0C60 Lo telu:vocalicll 0C61 Lo telu:vocaliclsign 0C62 Mn telu:vocalicllsign 0C63 Mn telu:zero 0C66 Nd telu:one 0C67 Nd telu:two 0C68 Nd telu:three 0C69 Nd telu:four 0C6A Nd telu:five 0C6B Nd telu:six 0C6C Nd telu:seven 0C6D Nd telu:eight 0C6E Nd telu:nine 0C6F Nd telu:siddhamsign 0C77 Po telu:fractionzeroforoddpowersoffour 0C78 No telu:fractiononeforoddpowersoffour 0C79 No telu:fractiontwoforoddpowersoffour 0C7A No telu:fractionthreeforoddpowersoffour 0C7B No telu:fractiononeforevenpowersoffour 0C7C No telu:fractiontwoforevenpowersoffour 0C7D No telu:fractionthreeforevenpowersoffour 0C7E No telu:tuumusign 0C7F So # Kannada knda:signspacingcandrabindu 0C80 Lo knda:signcandrabindu 0C81 Mn knda:anusvara 0C82 Mc knda:visarga 0C83 Mc knda:signsiddham 0C84 Po knda:a 0C85 Lo knda:aa 0C86 Lo knda:i 0C87 Lo knda:ii 0C88 Lo knda:u 0C89 Lo knda:uu 0C8A Lo knda:rvocal 0C8B Lo knda:lvocal 0C8C Lo knda:e 0C8E Lo knda:ee 0C8F Lo knda:ai 0C90 Lo knda:o 0C92 Lo knda:oo 0C93 Lo knda:au 0C94 Lo knda:ka 0C95 Lo knda:kha 0C96 Lo knda:ga 0C97 Lo knda:gha 0C98 Lo knda:nga 0C99 Lo knda:ca 0C9A Lo knda:cha 0C9B Lo knda:ja 0C9C Lo knda:jha 0C9D Lo knda:nya 0C9E Lo knda:tta 0C9F Lo knda:ttha 0CA0 Lo knda:dda 0CA1 Lo knda:ddha 0CA2 Lo knda:nna 0CA3 Lo knda:ta 0CA4 Lo knda:tha 0CA5 Lo knda:da 0CA6 Lo knda:dha 0CA7 Lo knda:na 0CA8 Lo knda:pa 0CAA Lo knda:pha 0CAB Lo knda:ba 0CAC Lo knda:bha 0CAD Lo knda:ma 0CAE Lo knda:ya 0CAF Lo knda:ra 0CB0 Lo knda:rra 0CB1 Lo knda:la 0CB2 Lo knda:lla 0CB3 Lo knda:va 0CB5 Lo knda:sha 0CB6 Lo knda:ssa 0CB7 Lo knda:sa 0CB8 Lo knda:ha 0CB9 Lo knda:nukta 0CBC Mn knda:avagraha 0CBD Lo knda:aasign 0CBE Mc knda:isign 0CBF Mn knda:iisign 0CC0 Mc knda:usign 0CC1 Mc knda:uusign 0CC2 Mc knda:rvocalsign 0CC3 Mc knda:rrvocalsign 0CC4 Mc knda:esign 0CC6 Mn knda:eesign 0CC7 Mc knda:aisign 0CC8 Mc knda:osign 0CCA Mc knda:oosign 0CCB Mc knda:ausign 0CCC Mn knda:virama 0CCD Mn knda:length 0CD5 Mc knda:ailength 0CD6 Mc knda:fa 0CDE Lo knda:rrvocal 0CE0 Lo knda:llvocal 0CE1 Lo knda:lvocalsign 0CE2 Mn knda:llvocalsign 0CE3 Mn knda:zero 0CE6 Nd knda:one 0CE7 Nd knda:two 0CE8 Nd knda:three 0CE9 Nd knda:four 0CEA Nd knda:five 0CEB Nd knda:six 0CEC Nd knda:seven 0CED Nd knda:eight 0CEE Nd knda:nine 0CEF Nd knda:jihvamuliya 0CF1 Lo knda:upadhmaniya 0CF2 Lo # Malayalam mlym:combininganusvaraabovesign 0D00 Mn mlym:candrabindusign 0D01 Mn mlym:anusvarasign 0D02 Mc mlym:visargasign 0D03 Mc mlym:a 0D05 Lo mlym:aa 0D06 Lo mlym:i 0D07 Lo mlym:ii 0D08 Lo mlym:u 0D09 Lo mlym:uu 0D0A Lo mlym:rvocal 0D0B Lo mlym:lvocal 0D0C Lo mlym:e 0D0E Lo mlym:ee 0D0F Lo mlym:ai 0D10 Lo mlym:o 0D12 Lo mlym:oo 0D13 Lo mlym:au 0D14 Lo mlym:ka 0D15 Lo mlym:kha 0D16 Lo mlym:ga 0D17 Lo mlym:gha 0D18 Lo mlym:nga 0D19 Lo mlym:ca 0D1A Lo mlym:cha 0D1B Lo mlym:ja 0D1C Lo mlym:jha 0D1D Lo mlym:nya 0D1E Lo mlym:tta 0D1F Lo mlym:ttha 0D20 Lo mlym:dda 0D21 Lo mlym:ddha 0D22 Lo mlym:nna 0D23 Lo mlym:ta 0D24 Lo mlym:tha 0D25 Lo mlym:da 0D26 Lo mlym:dha 0D27 Lo mlym:na 0D28 Lo mlym:nnna 0D29 Lo mlym:pa 0D2A Lo mlym:pha 0D2B Lo mlym:ba 0D2C Lo mlym:bha 0D2D Lo mlym:ma 0D2E Lo mlym:ya 0D2F Lo mlym:ra 0D30 Lo mlym:rra 0D31 Lo mlym:la 0D32 Lo mlym:lla 0D33 Lo mlym:llla 0D34 Lo mlym:va 0D35 Lo mlym:sha 0D36 Lo mlym:ssa 0D37 Lo mlym:sa 0D38 Lo mlym:ha 0D39 Lo mlym:ttta 0D3A Lo mlym:verticalbarviramasign 0D3B Mn mlym:circularviramasign 0D3C Mn mlym:avagrahasign 0D3D Lo mlym:aasign 0D3E Mc mlym:isign 0D3F Mc mlym:iisign 0D40 Mc mlym:usign 0D41 Mn mlym:uusign 0D42 Mn mlym:rvocalsign 0D43 Mn mlym:rrvocalsign 0D44 Mn mlym:esign 0D46 Mc mlym:eesign 0D47 Mc mlym:aisign 0D48 Mc mlym:osign 0D4A Mc mlym:oosign 0D4B Mc mlym:ausign 0D4C Mc mlym:viramasign 0D4D Mn mlym:dotreph 0D4E Lo mlym:parasign 0D4F So mlym:mchillu 0D54 Lo mlym:ychillu 0D55 Lo mlym:lllchillu 0D56 Lo mlym:aulength 0D57 Mc mlym:oneone-hundred-and-sixtieth 0D58 No mlym:onefortieth 0D59 No mlym:threeeightieths 0D5A No mlym:onetwentieth 0D5B No mlym:onetenth 0D5C No mlym:threetwentieths 0D5D No mlym:onefifth 0D5E No mlym:archaicii 0D5F Lo mlym:rrvocal 0D60 Lo mlym:llvocal 0D61 Lo mlym:lvocalsign 0D62 Mn mlym:llvocalsign 0D63 Mn mlym:zero 0D66 Nd mlym:one 0D67 Nd mlym:two 0D68 Nd mlym:three 0D69 Nd mlym:four 0D6A Nd mlym:five 0D6B Nd mlym:six 0D6C Nd mlym:seven 0D6D Nd mlym:eight 0D6E Nd mlym:nine 0D6F Nd mlym:ten 0D70 No mlym:onehundred 0D71 No mlym:onethousand 0D72 No mlym:onequarter 0D73 No mlym:onehalf 0D74 No mlym:threequarters 0D75 No mlym:onesixteenth 0D76 No mlym:oneeighth 0D77 No mlym:threesixteenths 0D78 No mlym:date 0D79 So mlym:nnchillu 0D7A Lo mlym:nchillu 0D7B Lo mlym:rrchillu 0D7C Lo mlym:lchillu 0D7D Lo mlym:llchillu 0D7E Lo mlym:kchillu 0D7F Lo # Sinhala sinh:anusvara 0D82 Mc sinh:visarga 0D83 Mc sinh:a 0D85 Lo sinh:aa 0D86 Lo sinh:ae 0D87 Lo sinh:aae 0D88 Lo sinh:i 0D89 Lo sinh:ii 0D8A Lo sinh:u 0D8B Lo sinh:uu 0D8C Lo sinh:vocalicr 0D8D Lo sinh:vocalicrr 0D8E Lo sinh:vocalicl 0D8F Lo sinh:vocalicll 0D90 Lo sinh:e 0D91 Lo sinh:ee 0D92 Lo sinh:ai 0D93 Lo sinh:o 0D94 Lo sinh:oo 0D95 Lo sinh:au 0D96 Lo sinh:ka 0D9A Lo sinh:kha 0D9B Lo sinh:ga 0D9C Lo sinh:gha 0D9D Lo sinh:nga 0D9E Lo sinh:nnga 0D9F Lo sinh:ca 0DA0 Lo sinh:cha 0DA1 Lo sinh:ja 0DA2 Lo sinh:jha 0DA3 Lo sinh:nya 0DA4 Lo sinh:jnya 0DA5 Lo sinh:nyja 0DA6 Lo sinh:tta 0DA7 Lo sinh:ttha 0DA8 Lo sinh:dda 0DA9 Lo sinh:ddha 0DAA Lo sinh:nna 0DAB Lo sinh:nndda 0DAC Lo sinh:ta 0DAD Lo sinh:tha 0DAE Lo sinh:da 0DAF Lo sinh:dha 0DB0 Lo sinh:na 0DB1 Lo sinh:nda 0DB3 Lo sinh:pa 0DB4 Lo sinh:pha 0DB5 Lo sinh:ba 0DB6 Lo sinh:bha 0DB7 Lo sinh:ma 0DB8 Lo sinh:mba 0DB9 Lo sinh:ya 0DBA Lo sinh:ra 0DBB Lo sinh:la 0DBD Lo sinh:va 0DC0 Lo sinh:sha 0DC1 Lo sinh:ssa 0DC2 Lo sinh:sa 0DC3 Lo sinh:ha 0DC4 Lo sinh:lla 0DC5 Lo sinh:fa 0DC6 Lo sinh:virama 0DCA Mn sinh:aasign 0DCF Mc sinh:aesign 0DD0 Mc sinh:aaesign 0DD1 Mc sinh:isign 0DD2 Mn sinh:iisign 0DD3 Mn sinh:usign 0DD4 Mn sinh:uusign 0DD6 Mn sinh:vocalicrsign 0DD8 Mc sinh:esign 0DD9 Mc sinh:eesign 0DDA Mc sinh:aisign 0DDB Mc sinh:osign 0DDC Mc sinh:oosign 0DDD Mc sinh:ausign 0DDE Mc sinh:vocaliclsign 0DDF Mc sinh:zerolith 0DE6 Nd sinh:onelith 0DE7 Nd sinh:twolith 0DE8 Nd sinh:threelith 0DE9 Nd sinh:fourlith 0DEA Nd sinh:fivelith 0DEB Nd sinh:sixlith 0DEC Nd sinh:sevenlith 0DED Nd sinh:eightlith 0DEE Nd sinh:ninelith 0DEF Nd sinh:vocalicrrsign 0DF2 Mc sinh:vocalicllsign 0DF3 Mc sinh:kunddaliya 0DF4 Po # Thai thai:kokai 0E01 Lo thai:khokhai 0E02 Lo thai:khokhuat 0E03 Lo thai:khokhwai 0E04 Lo thai:khokhon 0E05 Lo thai:khorakhang 0E06 Lo thai:ngongu 0E07 Lo thai:chochan 0E08 Lo thai:choching 0E09 Lo thai:chochang 0E0A Lo thai:soso 0E0B Lo thai:chochoe 0E0C Lo thai:yoying 0E0D Lo thai:dochada 0E0E Lo thai:topatak 0E0F Lo thai:thothan 0E10 Lo thai:thonangmontho 0E11 Lo thai:thophuthao 0E12 Lo thai:nonen 0E13 Lo thai:dodek 0E14 Lo thai:totao 0E15 Lo thai:thothung 0E16 Lo thai:thothahan 0E17 Lo thai:thothong 0E18 Lo thai:nonu 0E19 Lo thai:bobaimai 0E1A Lo thai:popla 0E1B Lo thai:phophung 0E1C Lo thai:fofa 0E1D Lo thai:phophan 0E1E Lo thai:fofan 0E1F Lo thai:phosamphao 0E20 Lo thai:moma 0E21 Lo thai:yoyak 0E22 Lo thai:rorua 0E23 Lo thai:ru 0E24 Lo thai:loling 0E25 Lo thai:lu 0E26 Lo thai:wowaen 0E27 Lo thai:sosala 0E28 Lo thai:sorusi 0E29 Lo thai:sosua 0E2A Lo thai:hohip 0E2B Lo thai:lochula 0E2C Lo thai:oang 0E2D Lo thai:honokhuk 0E2E Lo thai:paiyannoi 0E2F Lo thai:saraa 0E30 Lo thai:maihan-akat 0E31 Mn thai:saraaa 0E32 Lo thai:saraam 0E33 Lo thai:sarai 0E34 Mn thai:saraii 0E35 Mn thai:saraue 0E36 Mn thai:sarauee 0E37 Mn thai:sarau 0E38 Mn thai:sarauu 0E39 Mn thai:phinthu 0E3A Mn thai:baht 0E3F Sc thai:sarae 0E40 Lo thai:saraae 0E41 Lo thai:sarao 0E42 Lo thai:saraaimaimuan 0E43 Lo thai:saraaimaimalai 0E44 Lo thai:lakkhangyao 0E45 Lo thai:maiyamok 0E46 Lm thai:maitaikhu 0E47 Mn thai:maiek 0E48 Mn thai:maitho 0E49 Mn thai:maitri 0E4A Mn thai:maichattawa 0E4B Mn thai:thanthakhat 0E4C Mn thai:nikhahit 0E4D Mn thai:yamakkan 0E4E Mn thai:fongman 0E4F Po thai:zero 0E50 Nd thai:one 0E51 Nd thai:two 0E52 Nd thai:three 0E53 Nd thai:four 0E54 Nd thai:five 0E55 Nd thai:six 0E56 Nd thai:seven 0E57 Nd thai:eight 0E58 Nd thai:nine 0E59 Nd thai:angkhankhu 0E5A Po thai:khomut 0E5B Po # Tibetan tibt:omsyllable 0F00 Lo tibt:gteryigmgotruncatedamark 0F01 So tibt:gteryigmgoumrnambcadmamark 0F02 So tibt:gteryigmgoumgtertshegmamark 0F03 So tibt:yigmgomdunmainitialmark 0F04 Po tibt:yigmgosgabmaclosingmark 0F05 Po tibt:caretyigmgophurshadmamark 0F06 Po tibt:yigmgotshegshadmamark 0F07 Po tibt:sbrulshadmark 0F08 Po tibt:bskuryigmgomark 0F09 Po tibt:bkashogyigmgomark 0F0A Po tibt:intersyllabictshegmark 0F0B Po tibt:delimitertshegbstarmark 0F0C Po tibt:shadmark 0F0D Po tibt:nyisshadmark 0F0E Po tibt:tshegshadmark 0F0F Po tibt:nyistshegshadmark 0F10 Po tibt:rinchenspungsshadmark 0F11 Po tibt:rgyagramshadmark 0F12 Po tibt:caretdzudrtagsmelongcanmark 0F13 So tibt:gtertshegmark 0F14 Po tibt:chadrtagslogotypesign 0F15 So tibt:lhagrtagslogotypesign 0F16 So tibt:astrologicalsgragcancharrtagssign 0F17 So tibt:astrologicalkhyudpasign 0F18 Mn tibt:astrologicalsdongtshugssign 0F19 Mn tibt:rdeldkargcigsign 0F1A So tibt:rdeldkargnyissign 0F1B So tibt:rdeldkargsumsign 0F1C So tibt:rdelnaggcigsign 0F1D So tibt:rdelnaggnyissign 0F1E So tibt:rdeldkarrdelnagsign 0F1F So tibt:zero 0F20 Nd tibt:one 0F21 Nd tibt:two 0F22 Nd tibt:three 0F23 Nd tibt:four 0F24 Nd tibt:five 0F25 Nd tibt:six 0F26 Nd tibt:seven 0F27 Nd tibt:eight 0F28 Nd tibt:nine 0F29 Nd tibt:halfone 0F2A No tibt:halftwo 0F2B No tibt:halfthree 0F2C No tibt:halffour 0F2D No tibt:halffive 0F2E No tibt:halfsix 0F2F No tibt:halfseven 0F30 No tibt:halfeight 0F31 No tibt:halfnine 0F32 No tibt:halfzero 0F33 No tibt:bsdusrtagsmark 0F34 So tibt:ngasbzungnyizlamark 0F35 Mn tibt:caretdzudrtagsbzhimigcanmark 0F36 So tibt:ngasbzungsgorrtagsmark 0F37 Mn tibt:chemgomark 0F38 So tibt:tsaphrumark 0F39 Mn tibt:gugrtagsgyonmark 0F3A Ps tibt:gugrtagsgyasmark 0F3B Pe tibt:angkhanggyonmark 0F3C Ps tibt:angkhanggyasmark 0F3D Pe tibt:yartshessign 0F3E Mc tibt:martshessign 0F3F Mc tibt:ka 0F40 Lo tibt:kha 0F41 Lo tibt:ga 0F42 Lo tibt:gha 0F43 Lo tibt:nga 0F44 Lo tibt:ca 0F45 Lo tibt:cha 0F46 Lo tibt:ja 0F47 Lo tibt:nya 0F49 Lo tibt:tta 0F4A Lo tibt:ttha 0F4B Lo tibt:dda 0F4C Lo tibt:ddha 0F4D Lo tibt:nna 0F4E Lo tibt:ta 0F4F Lo tibt:tha 0F50 Lo tibt:da 0F51 Lo tibt:dha 0F52 Lo tibt:na 0F53 Lo tibt:pa 0F54 Lo tibt:pha 0F55 Lo tibt:ba 0F56 Lo tibt:bha 0F57 Lo tibt:ma 0F58 Lo tibt:tsa 0F59 Lo tibt:tsha 0F5A Lo tibt:dza 0F5B Lo tibt:dzha 0F5C Lo tibt:wa 0F5D Lo tibt:zha 0F5E Lo tibt:za 0F5F Lo tibt:AA 0F60 Lo tibt:ya 0F61 Lo tibt:ra 0F62 Lo tibt:la 0F63 Lo tibt:sha 0F64 Lo tibt:ssa 0F65 Lo tibt:sa 0F66 Lo tibt:ha 0F67 Lo tibt:a 0F68 Lo tibt:kssa 0F69 Lo tibt:rafixed 0F6A Lo tibt:kka 0F6B Lo tibt:rra 0F6C Lo tibt:aavowelsign 0F71 Mn tibt:ivowelsign 0F72 Mn tibt:iivowelsign 0F73 Mn tibt:uvowelsign 0F74 Mn tibt:uuvowelsign 0F75 Mn tibt:rvocalicvowelsign 0F76 Mn tibt:rrvocalicvowelsign 0F77 Mn tibt:lvocalicvowelsign 0F78 Mn tibt:llvocalicvowelsign 0F79 Mn tibt:evowelsign 0F7A Mn tibt:eevowelsign 0F7B Mn tibt:ovowelsign 0F7C Mn tibt:oovowelsign 0F7D Mn tibt:rjessungarosign 0F7E Mn tibt:rnambcadsign 0F7F Mc tibt:reversedivowelsign 0F80 Mn tibt:reversediivowelsign 0F81 Mn tibt:nyizlanaadasign 0F82 Mn tibt:snaldansign 0F83 Mn tibt:halantamark 0F84 Mn tibt:palutamark 0F85 Po tibt:lcirtagssign 0F86 Mn tibt:yangrtagssign 0F87 Mn tibt:lcetsacansign 0F88 Lo tibt:mchucansign 0F89 Lo tibt:grucanrgyingssign 0F8A Lo tibt:grumedrgyingssign 0F8B Lo tibt:invertedmchucansign 0F8C Lo tibt:lcetsacansubjoinedsign 0F8D Mn tibt:mchucansubjoinedsign 0F8E Mn tibt:invertedmchucansubjoinedsign 0F8F Mn tibt:kasubjoined 0F90 Mn tibt:khasubjoined 0F91 Mn tibt:gasubjoined 0F92 Mn tibt:ghasubjoined 0F93 Mn tibt:ngasubjoined 0F94 Mn tibt:casubjoined 0F95 Mn tibt:chasubjoined 0F96 Mn tibt:jasubjoined 0F97 Mn tibt:nyasubjoined 0F99 Mn tibt:ttasubjoined 0F9A Mn tibt:tthasubjoined 0F9B Mn tibt:ddasubjoined 0F9C Mn tibt:ddhasubjoined 0F9D Mn tibt:nnasubjoined 0F9E Mn tibt:tasubjoined 0F9F Mn tibt:thasubjoined 0FA0 Mn tibt:dasubjoined 0FA1 Mn tibt:dhasubjoined 0FA2 Mn tibt:nasubjoined 0FA3 Mn tibt:pasubjoined 0FA4 Mn tibt:phasubjoined 0FA5 Mn tibt:basubjoined 0FA6 Mn tibt:bhasubjoined 0FA7 Mn tibt:masubjoined 0FA8 Mn tibt:tsasubjoined 0FA9 Mn tibt:tshasubjoined 0FAA Mn tibt:dzasubjoined 0FAB Mn tibt:dzhasubjoined 0FAC Mn tibt:wasubjoined 0FAD Mn tibt:zhasubjoined 0FAE Mn tibt:zasubjoined 0FAF Mn tibt:subjoinedAA 0FB0 Mn tibt:yasubjoined 0FB1 Mn tibt:rasubjoined 0FB2 Mn tibt:lasubjoined 0FB3 Mn tibt:shasubjoined 0FB4 Mn tibt:ssasubjoined 0FB5 Mn tibt:sasubjoined 0FB6 Mn tibt:hasubjoined 0FB7 Mn tibt:asubjoined 0FB8 Mn tibt:kssasubjoined 0FB9 Mn tibt:wasubjoinedfixed 0FBA Mn tibt:yasubjoinedfixed 0FBB Mn tibt:rasubjoinedfixed 0FBC Mn tibt:kurukha 0FBE So tibt:kurukhabzhimigcan 0FBF So tibt:heavybeatcantillationsign 0FC0 So tibt:lightbeatcantillationsign 0FC1 So tibt:cangteucantillationsign 0FC2 So tibt:sbubchalcantillationsign 0FC3 So tibt:drilbusymbol 0FC4 So tibt:rdorjesymbol 0FC5 So tibt:padmagdansymbol 0FC6 Mn tibt:rdorjergyagramsymbol 0FC7 So tibt:phurpasymbol 0FC8 So tibt:norbusymbol 0FC9 So tibt:norbunyiskhyilsymbol 0FCA So tibt:norbugsumkhyilsymbol 0FCB So tibt:norbubzhikhyilsymbol 0FCC So tibt:rdelnagrdeldkarsign 0FCE So tibt:rdelnaggsumsign 0FCF So tibt:bskashoggimgorgyanmark 0FD0 Po tibt:mnyamyiggimgorgyanmark 0FD1 Po tibt:nyistshegmark 0FD2 Po tibt:brdarnyingyigmgomdunmainitialmark 0FD3 Po tibt:brdarnyingyigmgosgabmaclosingmark 0FD4 Po tibt:svastiright 0FD5 So tibt:svastileft 0FD6 So tibt:svastirightdot 0FD7 So tibt:svastileftdot 0FD8 So tibt:leadingmchanrtagsmark 0FD9 Po tibt:trailingmchanrtagsmark 0FDA Po # Georgian AnGeok 10A0 Lu BanGeok 10A1 Lu GanGeok 10A2 Lu DonGeok 10A3 Lu EnGeok 10A4 Lu VinGeok 10A5 Lu ZenGeok 10A6 Lu TanGeok 10A7 Lu InGeok 10A8 Lu KanGeok 10A9 Lu LasGeok 10AA Lu ManGeok 10AB Lu NarGeok 10AC Lu OnGeok 10AD Lu ParGeok 10AE Lu ZharGeok 10AF Lu RaeGeok 10B0 Lu SanGeok 10B1 Lu TarGeok 10B2 Lu UnGeok 10B3 Lu PharGeok 10B4 Lu KharGeok 10B5 Lu GhanGeok 10B6 Lu QarGeok 10B7 Lu ShinGeok 10B8 Lu ChinGeok 10B9 Lu CanGeok 10BA Lu JilGeok 10BB Lu CilGeok 10BC Lu CharGeok 10BD Lu XanGeok 10BE Lu JhanGeok 10BF Lu HaeGeok 10C0 Lu HeGeok 10C1 Lu HieGeok 10C2 Lu WeGeok 10C3 Lu HarGeok 10C4 Lu HoeGeok 10C5 Lu YnGeok 10C7 Lu AenGeok 10CD Lu anGeor 10D0 Ll banGeor 10D1 Ll ganGeor 10D2 Ll donGeor 10D3 Ll enGeor 10D4 Ll vinGeor 10D5 Ll zenGeor 10D6 Ll tanGeor 10D7 Ll inGeor 10D8 Ll kanGeor 10D9 Ll lasGeor 10DA Ll manGeor 10DB Ll narGeor 10DC Ll onGeor 10DD Ll parGeor 10DE Ll zharGeor 10DF Ll raeGeor 10E0 Ll sanGeor 10E1 Ll tarGeor 10E2 Ll unGeor 10E3 Ll pharGeor 10E4 Ll kharGeor 10E5 Ll ghanGeor 10E6 Ll qarGeor 10E7 Ll shinGeor 10E8 Ll chinGeor 10E9 Ll canGeor 10EA Ll jilGeor 10EB Ll cilGeor 10EC Ll charGeor 10ED Ll xanGeor 10EE Ll jhanGeor 10EF Ll haeGeor 10F0 Ll heGeor 10F1 Ll hieGeor 10F2 Ll weGeor 10F3 Ll harGeor 10F4 Ll hoeGeor 10F5 Ll fiGeor 10F6 Ll ynGeor 10F7 Ll elifiGeor 10F8 Ll turnedganGeor 10F9 Ll ainGeor 10FA Ll geor:paragraphseparator 10FB Po narmodGeor 10FC Lm aenGeor 10FD Ll hardsignGeor 10FE Ll labialsignGeor 10FF Ll # Hangul Jamo ko:kiyeokchoseong 1100 Lo ko:ssangkiyeokchoseong 1101 Lo ko:nieunchoseong 1102 Lo ko:tikeutchoseong 1103 Lo ko:ssangtikeutchoseong 1104 Lo ko:rieulchoseong 1105 Lo ko:mieumchoseong 1106 Lo ko:pieupchoseong 1107 Lo ko:ssangpieupchoseong 1108 Lo ko:sioschoseong 1109 Lo ko:ssangsioschoseong 110A Lo ko:ieungchoseong 110B Lo ko:cieucchoseong 110C Lo ko:ssangcieucchoseong 110D Lo ko:chieuchchoseong 110E Lo ko:khieukhchoseong 110F Lo ko:thieuthchoseong 1110 Lo ko:phieuphchoseong 1111 Lo ko:hieuhchoseong 1112 Lo ko:nieunkiyeokchoseong 1113 Lo ko:ssangnieunchoseong 1114 Lo ko:nieuntikeutchoseong 1115 Lo ko:nieunpieupchoseong 1116 Lo ko:tikeutkiyeokchoseong 1117 Lo ko:rieulnieunchoseong 1118 Lo ko:ssangrieulchoseong 1119 Lo ko:rieulhieuhchoseong 111A Lo ko:kapyeounrieulchoseong 111B Lo ko:mieumpieupchoseong 111C Lo ko:kapyeounmieumchoseong 111D Lo ko:pieupkiyeokchoseong 111E Lo ko:pieupnieunchoseong 111F Lo ko:pieuptikeutchoseong 1120 Lo ko:pieupsioschoseong 1121 Lo ko:pieupsioskiyeokchoseong 1122 Lo ko:pieupsiostikeutchoseong 1123 Lo ko:pieupsiospieupchoseong 1124 Lo ko:pieupssangsioschoseong 1125 Lo ko:pieupsioscieucchoseong 1126 Lo ko:pieupcieucchoseong 1127 Lo ko:pieupchieuchchoseong 1128 Lo ko:pieupthieuthchoseong 1129 Lo ko:pieupphieuphchoseong 112A Lo ko:kapyeounpieupchoseong 112B Lo ko:kapyeounssangpieupchoseong 112C Lo ko:sioskiyeokchoseong 112D Lo ko:siosnieunchoseong 112E Lo ko:siostikeutchoseong 112F Lo ko:siosrieulchoseong 1130 Lo ko:siosmieumchoseong 1131 Lo ko:siospieupchoseong 1132 Lo ko:siospieupkiyeokchoseong 1133 Lo ko:siosssangsioschoseong 1134 Lo ko:siosieungchoseong 1135 Lo ko:sioscieucchoseong 1136 Lo ko:sioschieuchchoseong 1137 Lo ko:sioskhieukhchoseong 1138 Lo ko:siosthieuthchoseong 1139 Lo ko:siosphieuphchoseong 113A Lo ko:sioshieuhchoseong 113B Lo ko:chitueumsioschoseong 113C Lo ko:chitueumssangsioschoseong 113D Lo ko:ceongchieumsioschoseong 113E Lo ko:ceongchieumssangsioschoseong 113F Lo ko:pansioschoseong 1140 Lo ko:ieungkiyeokchoseong 1141 Lo ko:ieungtikeutchoseong 1142 Lo ko:ieungmieumchoseong 1143 Lo ko:ieungpieupchoseong 1144 Lo ko:ieungsioschoseong 1145 Lo ko:ieungpansioschoseong 1146 Lo ko:ssangieungchoseong 1147 Lo ko:ieungcieucchoseong 1148 Lo ko:ieungchieuchchoseong 1149 Lo ko:ieungthieuthchoseong 114A Lo ko:ieungphieuphchoseong 114B Lo ko:yesieungchoseong 114C Lo ko:cieucieungchoseong 114D Lo ko:chitueumcieucchoseong 114E Lo ko:chitueumssangcieucchoseong 114F Lo ko:ceongchieumcieucchoseong 1150 Lo ko:ceongchieumssangcieucchoseong 1151 Lo ko:chieuchkhieukhchoseong 1152 Lo ko:chieuchhieuhchoseong 1153 Lo ko:chitueumchieuchchoseong 1154 Lo ko:ceongchieumchieuchchoseong 1155 Lo ko:phieuphpieupchoseong 1156 Lo ko:kapyeounphieuphchoseong 1157 Lo ko:ssanghieuhchoseong 1158 Lo ko:yeorinhieuhchoseong 1159 Lo ko:kiyeoktikeutchoseong 115A Lo ko:nieunsioschoseong 115B Lo ko:nieuncieucchoseong 115C Lo ko:nieunhieuhchoseong 115D Lo ko:tikeutrieulchoseong 115E Lo ko:fillerchoseong 115F Lo ko:fillerjungseong 1160 Lo ko:ajungseong 1161 Lo ko:aejungseong 1162 Lo ko:yajungseong 1163 Lo ko:yaejungseong 1164 Lo ko:eojungseong 1165 Lo ko:ejungseong 1166 Lo ko:yeojungseong 1167 Lo ko:yejungseong 1168 Lo ko:ojungseong 1169 Lo ko:wajungseong 116A Lo ko:waejungseong 116B Lo ko:oejungseong 116C Lo ko:yojungseong 116D Lo ko:ujungseong 116E Lo ko:weojungseong 116F Lo ko:wejungseong 1170 Lo ko:wijungseong 1171 Lo ko:yujungseong 1172 Lo ko:eujungseong 1173 Lo ko:yijungseong 1174 Lo ko:ijungseong 1175 Lo ko:aojungseong 1176 Lo ko:aujungseong 1177 Lo ko:yaojungseong 1178 Lo ko:yayojungseong 1179 Lo ko:eoojungseong 117A Lo ko:eoujungseong 117B Lo ko:eo_eujungseong 117C Lo ko:yeoojungseong 117D Lo ko:yeoujungseong 117E Lo ko:o_eojungseong 117F Lo ko:o_ejungseong 1180 Lo ko:oyejungseong 1181 Lo ko:oojungseong 1182 Lo ko:oujungseong 1183 Lo ko:yoyajungseong 1184 Lo ko:yoyaejungseong 1185 Lo ko:yoyeojungseong 1186 Lo ko:yoojungseong 1187 Lo ko:yoijungseong 1188 Lo ko:uajungseong 1189 Lo ko:uaejungseong 118A Lo ko:ueo_eujungseong 118B Lo ko:uyejungseong 118C Lo ko:uujungseong 118D Lo ko:yuajungseong 118E Lo ko:yueojungseong 118F Lo ko:yuejungseong 1190 Lo ko:yuyeojungseong 1191 Lo ko:yuyejungseong 1192 Lo ko:yuujungseong 1193 Lo ko:yuijungseong 1194 Lo ko:euujungseong 1195 Lo ko:eueujungseong 1196 Lo ko:yiujungseong 1197 Lo ko:iajungseong 1198 Lo ko:iyajungseong 1199 Lo ko:iojungseong 119A Lo ko:iujungseong 119B Lo ko:ieujungseong 119C Lo ko:iaraeajungseong 119D Lo ko:araeajungseong 119E Lo ko:araeaeojungseong 119F Lo ko:araeaujungseong 11A0 Lo ko:araeaijungseong 11A1 Lo ko:ssangaraeajungseong 11A2 Lo ko:aeujungseong 11A3 Lo ko:yaujungseong 11A4 Lo ko:yeoyajungseong 11A5 Lo ko:oyajungseong 11A6 Lo ko:oyaejungseong 11A7 Lo ko:kiyeokjongseong 11A8 Lo ko:ssangkiyeokjongseong 11A9 Lo ko:kiyeoksiosjongseong 11AA Lo ko:nieunjongseong 11AB Lo ko:nieuncieucjongseong 11AC Lo ko:nieunhieuhjongseong 11AD Lo ko:tikeutjongseong 11AE Lo ko:rieuljongseong 11AF Lo ko:rieulkiyeokjongseong 11B0 Lo ko:rieulmieumjongseong 11B1 Lo ko:rieulpieupjongseong 11B2 Lo ko:rieulsiosjongseong 11B3 Lo ko:rieulthieuthjongseong 11B4 Lo ko:rieulphieuphjongseong 11B5 Lo ko:rieulhieuhjongseong 11B6 Lo ko:mieumjongseong 11B7 Lo ko:pieupjongseong 11B8 Lo ko:pieupsiosjongseong 11B9 Lo ko:siosjongseong 11BA Lo ko:ssangsiosjongseong 11BB Lo ko:ieungjongseong 11BC Lo ko:cieucjongseong 11BD Lo ko:chieuchjongseong 11BE Lo ko:khieukhjongseong 11BF Lo ko:thieuthjongseong 11C0 Lo ko:phieuphjongseong 11C1 Lo ko:hieuhjongseong 11C2 Lo ko:kiyeokrieuljongseong 11C3 Lo ko:kiyeoksioskiyeokjongseong 11C4 Lo ko:nieunkiyeokjongseong 11C5 Lo ko:nieuntikeutjongseong 11C6 Lo ko:nieunsiosjongseong 11C7 Lo ko:nieunpansiosjongseong 11C8 Lo ko:nieunthieuthjongseong 11C9 Lo ko:tikeutkiyeokjongseong 11CA Lo ko:tikeutrieuljongseong 11CB Lo ko:rieulkiyeoksiosjongseong 11CC Lo ko:rieulnieunjongseong 11CD Lo ko:rieultikeutjongseong 11CE Lo ko:rieultikeuthieuhjongseong 11CF Lo ko:ssangrieuljongseong 11D0 Lo ko:rieulmieumkiyeokjongseong 11D1 Lo ko:rieulmieumsiosjongseong 11D2 Lo ko:rieulpieupsiosjongseong 11D3 Lo ko:rieulpieuphieuhjongseong 11D4 Lo ko:rieulkapyeounpieupjongseong 11D5 Lo ko:rieulssangsiosjongseong 11D6 Lo ko:rieulpansiosjongseong 11D7 Lo ko:rieulkhieukhjongseong 11D8 Lo ko:rieulyeorinhieuhjongseong 11D9 Lo ko:mieumkiyeokjongseong 11DA Lo ko:mieumrieuljongseong 11DB Lo ko:mieumpieupjongseong 11DC Lo ko:mieumsiosjongseong 11DD Lo ko:mieumssangsiosjongseong 11DE Lo ko:mieumpansiosjongseong 11DF Lo ko:mieumchieuchjongseong 11E0 Lo ko:mieumhieuhjongseong 11E1 Lo ko:kapyeounmieumjongseong 11E2 Lo ko:pieuprieuljongseong 11E3 Lo ko:pieupphieuphjongseong 11E4 Lo ko:pieuphieuhjongseong 11E5 Lo ko:kapyeounpieupjongseong 11E6 Lo ko:sioskiyeokjongseong 11E7 Lo ko:siostikeutjongseong 11E8 Lo ko:siosrieuljongseong 11E9 Lo ko:siospieupjongseong 11EA Lo ko:pansiosjongseong 11EB Lo ko:ieungkiyeokjongseong 11EC Lo ko:ieungssangkiyeokjongseong 11ED Lo ko:ssangieungjongseong 11EE Lo ko:ieungkhieukhjongseong 11EF Lo ko:yesieungjongseong 11F0 Lo ko:yesieungsiosjongseong 11F1 Lo ko:yesieungpansiosjongseong 11F2 Lo ko:phieuphpieupjongseong 11F3 Lo ko:kapyeounphieuphjongseong 11F4 Lo ko:hieuhnieunjongseong 11F5 Lo ko:hieuhrieuljongseong 11F6 Lo ko:hieuhmieumjongseong 11F7 Lo ko:hieuhpieupjongseong 11F8 Lo ko:yeorinhieuhjongseong 11F9 Lo ko:kiyeoknieunjongseong 11FA Lo ko:kiyeokpieupjongseong 11FB Lo ko:kiyeokchieuchjongseong 11FC Lo ko:kiyeokkhieukhjongseong 11FD Lo ko:kiyeokhieuhjongseong 11FE Lo ko:ssangnieunjongseong 11FF Lo # Ethiopic ethi:ha 1200 Lo ethi:hu 1201 Lo ethi:hi 1202 Lo ethi:haa 1203 Lo ethi:hee 1204 Lo ethi:he 1205 Lo ethi:ho 1206 Lo ethi:hoa 1207 Lo ethi:la 1208 Lo ethi:lu 1209 Lo ethi:li 120A Lo ethi:laa 120B Lo ethi:lee 120C Lo ethi:le 120D Lo ethi:lo 120E Lo ethi:lwa 120F Lo ethi:hha 1210 Lo ethi:hhu 1211 Lo ethi:hhi 1212 Lo ethi:hhaa 1213 Lo ethi:hhee 1214 Lo ethi:hhe 1215 Lo ethi:hho 1216 Lo ethi:hhwa 1217 Lo ethi:ma 1218 Lo ethi:mu 1219 Lo ethi:mi 121A Lo ethi:maa 121B Lo ethi:mee 121C Lo ethi:me 121D Lo ethi:mo 121E Lo ethi:mwa 121F Lo ethi:sza 1220 Lo ethi:szu 1221 Lo ethi:szi 1222 Lo ethi:szaa 1223 Lo ethi:szee 1224 Lo ethi:sze 1225 Lo ethi:szo 1226 Lo ethi:szwa 1227 Lo ethi:ra 1228 Lo ethi:ru 1229 Lo ethi:ri 122A Lo ethi:raa 122B Lo ethi:ree 122C Lo ethi:re 122D Lo ethi:ro 122E Lo ethi:rwa 122F Lo ethi:sa 1230 Lo ethi:su 1231 Lo ethi:si 1232 Lo ethi:saa 1233 Lo ethi:see 1234 Lo ethi:se 1235 Lo ethi:so 1236 Lo ethi:swa 1237 Lo ethi:sha 1238 Lo ethi:shu 1239 Lo ethi:shi 123A Lo ethi:shaa 123B Lo ethi:shee 123C Lo ethi:she 123D Lo ethi:sho 123E Lo ethi:shwa 123F Lo ethi:qa 1240 Lo ethi:qu 1241 Lo ethi:qi 1242 Lo ethi:qaa 1243 Lo ethi:qee 1244 Lo ethi:qe 1245 Lo ethi:qo 1246 Lo ethi:qoa 1247 Lo ethi:qwa 1248 Lo ethi:qwi 124A Lo ethi:qwaa 124B Lo ethi:qwee 124C Lo ethi:qwe 124D Lo ethi:qha 1250 Lo ethi:qhu 1251 Lo ethi:qhi 1252 Lo ethi:qhaa 1253 Lo ethi:qhee 1254 Lo ethi:qhe 1255 Lo ethi:qho 1256 Lo ethi:qhwa 1258 Lo ethi:qhwi 125A Lo ethi:qhwaa 125B Lo ethi:qhwee 125C Lo ethi:qhwe 125D Lo ethi:ba 1260 Lo ethi:bu 1261 Lo ethi:bi 1262 Lo ethi:baa 1263 Lo ethi:bee 1264 Lo ethi:be 1265 Lo ethi:bo 1266 Lo ethi:bwa 1267 Lo ethi:va 1268 Lo ethi:vu 1269 Lo ethi:vi 126A Lo ethi:vaa 126B Lo ethi:vee 126C Lo ethi:ve 126D Lo ethi:vo 126E Lo ethi:vwa 126F Lo ethi:ta 1270 Lo ethi:tu 1271 Lo ethi:ti 1272 Lo ethi:taa 1273 Lo ethi:tee 1274 Lo ethi:te 1275 Lo ethi:to 1276 Lo ethi:twa 1277 Lo ethi:ca 1278 Lo ethi:cu 1279 Lo ethi:ci 127A Lo ethi:caa 127B Lo ethi:cee 127C Lo ethi:ce 127D Lo ethi:co 127E Lo ethi:cwa 127F Lo ethi:xa 1280 Lo ethi:xu 1281 Lo ethi:xi 1282 Lo ethi:xaa 1283 Lo ethi:xee 1284 Lo ethi:xe 1285 Lo ethi:xo 1286 Lo ethi:xoa 1287 Lo ethi:xwa 1288 Lo ethi:xwi 128A Lo ethi:xwaa 128B Lo ethi:xwee 128C Lo ethi:xwe 128D Lo ethi:na 1290 Lo ethi:nu 1291 Lo ethi:ni 1292 Lo ethi:naa 1293 Lo ethi:nee 1294 Lo ethi:ne 1295 Lo ethi:no 1296 Lo ethi:nwa 1297 Lo ethi:nya 1298 Lo ethi:nyu 1299 Lo ethi:nyi 129A Lo ethi:nyaa 129B Lo ethi:nyee 129C Lo ethi:nye 129D Lo ethi:nyo 129E Lo ethi:nywa 129F Lo ethi:aglottal 12A0 Lo ethi:uglottal 12A1 Lo ethi:iglottal 12A2 Lo ethi:aaglottal 12A3 Lo ethi:eeglottal 12A4 Lo ethi:eglottal 12A5 Lo ethi:oglottal 12A6 Lo ethi:waglottal 12A7 Lo ethi:ka 12A8 Lo ethi:ku 12A9 Lo ethi:ki 12AA Lo ethi:kaa 12AB Lo ethi:kee 12AC Lo ethi:ke 12AD Lo ethi:ko 12AE Lo ethi:koa 12AF Lo ethi:kwa 12B0 Lo ethi:kwi 12B2 Lo ethi:kwaa 12B3 Lo ethi:kwee 12B4 Lo ethi:kwe 12B5 Lo ethi:kxa 12B8 Lo ethi:kxu 12B9 Lo ethi:kxi 12BA Lo ethi:kxaa 12BB Lo ethi:kxee 12BC Lo ethi:kxe 12BD Lo ethi:kxo 12BE Lo ethi:kxwa 12C0 Lo ethi:kxwi 12C2 Lo ethi:kxwaa 12C3 Lo ethi:kxwee 12C4 Lo ethi:kxwe 12C5 Lo ethi:wa 12C8 Lo ethi:wu 12C9 Lo ethi:wi 12CA Lo ethi:waa 12CB Lo ethi:wee 12CC Lo ethi:we 12CD Lo ethi:wo 12CE Lo ethi:woa 12CF Lo ethi:pharyngeala 12D0 Lo ethi:pharyngealu 12D1 Lo ethi:pharyngeali 12D2 Lo ethi:pharyngealaa 12D3 Lo ethi:pharyngealee 12D4 Lo ethi:pharyngeale 12D5 Lo ethi:pharyngealo 12D6 Lo ethi:za 12D8 Lo ethi:zu 12D9 Lo ethi:zi 12DA Lo ethi:zaa 12DB Lo ethi:zee 12DC Lo ethi:ze 12DD Lo ethi:zo 12DE Lo ethi:zwa 12DF Lo ethi:zha 12E0 Lo ethi:zhu 12E1 Lo ethi:zhi 12E2 Lo ethi:zhaa 12E3 Lo ethi:zhee 12E4 Lo ethi:zhe 12E5 Lo ethi:zho 12E6 Lo ethi:zhwa 12E7 Lo ethi:ya 12E8 Lo ethi:yu 12E9 Lo ethi:yi 12EA Lo ethi:yaa 12EB Lo ethi:yee 12EC Lo ethi:ye 12ED Lo ethi:yo 12EE Lo ethi:yoa 12EF Lo ethi:da 12F0 Lo ethi:du 12F1 Lo ethi:di 12F2 Lo ethi:daa 12F3 Lo ethi:dee 12F4 Lo ethi:de 12F5 Lo ethi:do 12F6 Lo ethi:dwa 12F7 Lo ethi:dda 12F8 Lo ethi:ddu 12F9 Lo ethi:ddi 12FA Lo ethi:ddaa 12FB Lo ethi:ddee 12FC Lo ethi:dde 12FD Lo ethi:ddo 12FE Lo ethi:ddwa 12FF Lo ethi:ja 1300 Lo ethi:ju 1301 Lo ethi:ji 1302 Lo ethi:jaa 1303 Lo ethi:jee 1304 Lo ethi:je 1305 Lo ethi:jo 1306 Lo ethi:jwa 1307 Lo ethi:ga 1308 Lo ethi:gu 1309 Lo ethi:gi 130A Lo ethi:gaa 130B Lo ethi:gee 130C Lo ethi:ge 130D Lo ethi:go 130E Lo ethi:goa 130F Lo ethi:gwa 1310 Lo ethi:gwi 1312 Lo ethi:gwaa 1313 Lo ethi:gwee 1314 Lo ethi:gwe 1315 Lo ethi:gga 1318 Lo ethi:ggu 1319 Lo ethi:ggi 131A Lo ethi:ggaa 131B Lo ethi:ggee 131C Lo ethi:gge 131D Lo ethi:ggo 131E Lo ethi:ggwaa 131F Lo ethi:tha 1320 Lo ethi:thu 1321 Lo ethi:thi 1322 Lo ethi:thaa 1323 Lo ethi:thee 1324 Lo ethi:the 1325 Lo ethi:tho 1326 Lo ethi:thwa 1327 Lo ethi:cha 1328 Lo ethi:chu 1329 Lo ethi:chi 132A Lo ethi:chaa 132B Lo ethi:chee 132C Lo ethi:che 132D Lo ethi:cho 132E Lo ethi:chwa 132F Lo ethi:pha 1330 Lo ethi:phu 1331 Lo ethi:phi 1332 Lo ethi:phaa 1333 Lo ethi:phee 1334 Lo ethi:phe 1335 Lo ethi:pho 1336 Lo ethi:phwa 1337 Lo ethi:tsa 1338 Lo ethi:tsu 1339 Lo ethi:tsi 133A Lo ethi:tsaa 133B Lo ethi:tsee 133C Lo ethi:tse 133D Lo ethi:tso 133E Lo ethi:tswa 133F Lo ethi:tza 1340 Lo ethi:tzu 1341 Lo ethi:tzi 1342 Lo ethi:tzaa 1343 Lo ethi:tzee 1344 Lo ethi:tze 1345 Lo ethi:tzo 1346 Lo ethi:tzoa 1347 Lo ethi:fa 1348 Lo ethi:fu 1349 Lo ethi:fi 134A Lo ethi:faa 134B Lo ethi:fee 134C Lo ethi:fe 134D Lo ethi:fo 134E Lo ethi:fwa 134F Lo ethi:pa 1350 Lo ethi:pu 1351 Lo ethi:pi 1352 Lo ethi:paa 1353 Lo ethi:pee 1354 Lo ethi:pe 1355 Lo ethi:po 1356 Lo ethi:pwa 1357 Lo ethi:rya 1358 Lo ethi:mya 1359 Lo ethi:fya 135A Lo ethi:geminationandvowellengthmarkcmb 135D Mn ethi:vowellengthmarkcmb 135E Mn ethi:geminationmarkcmb 135F Mn ethi:sectionmark 1360 Po ethi:wordspace 1361 Po ethi:fullstop 1362 Po ethi:comma 1363 Po ethi:semicolon 1364 Po ethi:colon 1365 Po ethi:prefacecolon 1366 Po ethi:questionmark 1367 Po ethi:paragraphseparator 1368 Po ethi:one 1369 No ethi:two 136A No ethi:three 136B No ethi:four 136C No ethi:five 136D No ethi:six 136E No ethi:seven 136F No ethi:eight 1370 No ethi:nine 1371 No ethi:ten 1372 No ethi:twenty 1373 No ethi:thirty 1374 No ethi:forty 1375 No ethi:fifty 1376 No ethi:sixty 1377 No ethi:seventy 1378 No ethi:eighty 1379 No ethi:ninety 137A No ethi:hundred 137B No ethi:tenthousand 137C No # Cherokee cher:a 13A0 Lu cher:e 13A1 Lu cher:i 13A2 Lu cher:o 13A3 Lu cher:u 13A4 Lu cher:v 13A5 Lu cher:ga 13A6 Lu cher:ka 13A7 Lu cher:ge 13A8 Lu cher:gi 13A9 Lu cher:go 13AA Lu cher:gu 13AB Lu cher:gv 13AC Lu cher:ha 13AD Lu cher:he 13AE Lu cher:hi 13AF Lu cher:ho 13B0 Lu cher:hu 13B1 Lu cher:hv 13B2 Lu cher:la 13B3 Lu cher:le 13B4 Lu cher:li 13B5 Lu cher:lo 13B6 Lu cher:lu 13B7 Lu cher:lv 13B8 Lu cher:ma 13B9 Lu cher:me 13BA Lu cher:mi 13BB Lu cher:mo 13BC Lu cher:mu 13BD Lu cher:na 13BE Lu cher:hna 13BF Lu cher:nah 13C0 Lu cher:ne 13C1 Lu cher:ni 13C2 Lu cher:no 13C3 Lu cher:nu 13C4 Lu cher:nv 13C5 Lu cher:qua 13C6 Lu cher:que 13C7 Lu cher:qui 13C8 Lu cher:quo 13C9 Lu cher:quu 13CA Lu cher:quv 13CB Lu cher:sa 13CC Lu cher:s 13CD Lu cher:se 13CE Lu cher:si 13CF Lu cher:so 13D0 Lu cher:su 13D1 Lu cher:sv 13D2 Lu cher:da 13D3 Lu cher:ta 13D4 Lu cher:de 13D5 Lu cher:te 13D6 Lu cher:di 13D7 Lu cher:ti 13D8 Lu cher:do 13D9 Lu cher:du 13DA Lu cher:dv 13DB Lu cher:dla 13DC Lu cher:tla 13DD Lu cher:tle 13DE Lu cher:tli 13DF Lu cher:tlo 13E0 Lu cher:tlu 13E1 Lu cher:tlv 13E2 Lu cher:tsa 13E3 Lu cher:tse 13E4 Lu cher:tsi 13E5 Lu cher:tso 13E6 Lu cher:tsu 13E7 Lu cher:tsv 13E8 Lu cher:wa 13E9 Lu cher:we 13EA Lu cher:wi 13EB Lu cher:wo 13EC Lu cher:wu 13ED Lu cher:wv 13EE Lu cher:ya 13EF Lu cher:ye 13F0 Lu cher:yi 13F1 Lu cher:yo 13F2 Lu cher:yu 13F3 Lu cher:yv 13F4 Lu cher:mv 13F5 Lu cher:yesmall 13F8 Ll cher:yismall 13F9 Ll cher:yosmall 13FA Ll cher:yusmall 13FB Ll cher:yvsmall 13FC Ll cher:mvsmall 13FD Ll # Runic runr:fehu_feoh_fe_f 16A0 Lo runr:v 16A1 Lo runr:uruz_ur_u 16A2 Lo runr:yr 16A3 Lo runr:y 16A4 Lo runr:w 16A5 Lo runr:thurisaz_thurs_thorn 16A6 Lo runr:eth 16A7 Lo runr:ansuz_a 16A8 Lo runr:os_o 16A9 Lo runr:ac_a 16AA Lo runr:aesc 16AB Lo runr:long_branch_oss_o 16AC Lo runr:short_twig_oss_o 16AD Lo runr:o 16AE Lo runr:oe 16AF Lo runr:on 16B0 Lo runr:raido_rad_reid_r 16B1 Lo runr:kauna 16B2 Lo runr:cen 16B3 Lo runr:kaun_k 16B4 Lo runr:g 16B5 Lo runr:eng 16B6 Lo runr:gebo_gyfu_g 16B7 Lo runr:gar 16B8 Lo runr:wunjo_wynn_w 16B9 Lo runr:haglaz_h 16BA Lo runr:haegl_h 16BB Lo runr:long_branch_hagall_h 16BC Lo runr:short_twig_hagall_h 16BD Lo runr:naudiz_nyd_naud_n 16BE Lo runr:short_twig_naud_n 16BF Lo runr:dotted_n 16C0 Lo runr:isaz_is_iss_i 16C1 Lo runr:e 16C2 Lo runr:jeran_j 16C3 Lo runr:ger 16C4 Lo runr:long_branch_ar_ae 16C5 Lo runr:short_twig_ar_a 16C6 Lo runr:iwaz_eoh 16C7 Lo runr:pertho_peorth_p 16C8 Lo runr:algiz_eolhx 16C9 Lo runr:sowilo_s 16CA Lo runr:sigel_long_branch_sol_s 16CB Lo runr:short_twig_sol_s 16CC Lo runr:c 16CD Lo runr:z 16CE Lo runr:tiwaz_tir_tyr_t 16CF Lo runr:short_twig_tyr_t 16D0 Lo runr:d 16D1 Lo runr:berkanan_beorc_bjarkan_b 16D2 Lo runr:short_twig_bjarkan_b 16D3 Lo runr:dotted_p 16D4 Lo runr:open_p 16D5 Lo runr:ehwaz_eh_e 16D6 Lo runr:mannaz_man_m 16D7 Lo runr:long_branch_madr_m 16D8 Lo runr:short_twig_madr_m 16D9 Lo runr:laukaz_lagu_logr_l 16DA Lo runr:dotted_l 16DB Lo runr:ingwaz 16DC Lo runr:ing 16DD Lo runr:dagaz_daeg_d 16DE Lo runr:othalan_ethel_o 16DF Lo runr:ear 16E0 Lo runr:ior 16E1 Lo runr:cweorth 16E2 Lo runr:calc 16E3 Lo runr:cealc 16E4 Lo runr:stan 16E5 Lo runr:long_branch_yr 16E6 Lo runr:short_twig_yr 16E7 Lo runr:icelandic_yr 16E8 Lo runr:q 16E9 Lo runr:x 16EA Lo runr:single_punctuation 16EB Po runr:multiple_punctuation 16EC Po runr:cross_punctuation 16ED Po runr:arlaug_symbol 16EE Nl runr:tvimadur_symbol 16EF Nl runr:belgthor_symbol 16F0 Nl runr:k 16F1 Lo runr:sh 16F2 Lo runr:oo 16F3 Lo runr:franks_casket_os 16F4 Lo runr:franks_casket_is 16F5 Lo runr:franks_casket_eh 16F6 Lo runr:franks_casket_ac 16F7 Lo runr:franks_casket_aesc 16F8 Lo # Mongolian mong:birga 1800 Po mong:ellipsis 1801 Po mong:comma 1802 Po mong:period 1803 Po mong:colon 1804 Po mong:fourdots 1805 Po mong:softhyphentodo 1806 Pd mong:syllableboundarymarkersibe 1807 Po mong:commamanchu 1808 Po mong:periodmanchu 1809 Po mong:nirugu 180A Po mong:freevariationselectorone 180B Mn mong:freevariationselectortwo 180C Mn mong:freevariationselectorthree 180D Mn mong:vowelseparator 180E Cf mong:zero 1810 Nd mong:one 1811 Nd mong:two 1812 Nd mong:three 1813 Nd mong:four 1814 Nd mong:five 1815 Nd mong:six 1816 Nd mong:seven 1817 Nd mong:eight 1818 Nd mong:nine 1819 Nd mong:a 1820 Lo mong:e 1821 Lo mong:i 1822 Lo mong:o 1823 Lo mong:u 1824 Lo mong:oe 1825 Lo mong:ue 1826 Lo mong:ee 1827 Lo mong:na 1828 Lo mong:ang 1829 Lo mong:ba 182A Lo mong:pa 182B Lo mong:qa 182C Lo mong:ga 182D Lo mong:ma 182E Lo mong:la 182F Lo mong:sa 1830 Lo mong:sha 1831 Lo mong:ta 1832 Lo mong:da 1833 Lo mong:cha 1834 Lo mong:ja 1835 Lo mong:ya 1836 Lo mong:ra 1837 Lo mong:wa 1838 Lo mong:fa 1839 Lo mong:ka 183A Lo mong:kha 183B Lo mong:tsa 183C Lo mong:za 183D Lo mong:haa 183E Lo mong:zra 183F Lo mong:lha 1840 Lo mong:zhi 1841 Lo mong:chi 1842 Lo mong:longvowelsigntodo 1843 Lm mong:etodo 1844 Lo mong:itodo 1845 Lo mong:otodo 1846 Lo mong:utodo 1847 Lo mong:oetodo 1848 Lo mong:uetodo 1849 Lo mong:angtodo 184A Lo mong:batodo 184B Lo mong:patodo 184C Lo mong:qatodo 184D Lo mong:gatodo 184E Lo mong:matodo 184F Lo mong:tatodo 1850 Lo mong:datodo 1851 Lo mong:chatodo 1852 Lo mong:jatodo 1853 Lo mong:tsatodo 1854 Lo mong:yatodo 1855 Lo mong:watodo 1856 Lo mong:katodo 1857 Lo mong:gaatodo 1858 Lo mong:haatodo 1859 Lo mong:jiatodo 185A Lo mong:niatodo 185B Lo mong:dzatodo 185C Lo mong:esibe 185D Lo mong:isibe 185E Lo mong:iysibe 185F Lo mong:uesibe 1860 Lo mong:usibe 1861 Lo mong:angsibe 1862 Lo mong:kasibe 1863 Lo mong:gasibe 1864 Lo mong:hasibe 1865 Lo mong:pasibe 1866 Lo mong:shasibe 1867 Lo mong:tasibe 1868 Lo mong:dasibe 1869 Lo mong:jasibe 186A Lo mong:fasibe 186B Lo mong:gaasibe 186C Lo mong:haasibe 186D Lo mong:tsasibe 186E Lo mong:zasibe 186F Lo mong:raasibe 1870 Lo mong:chasibe 1871 Lo mong:zhasibe 1872 Lo mong:imanchu 1873 Lo mong:kamanchu 1874 Lo mong:ramanchu 1875 Lo mong:famanchu 1876 Lo mong:zhamanchu 1877 Lo mong:chawithtwodots 1878 Lo mong:anusvaraonealigali 1880 Lo mong:visargaonealigali 1881 Lo mong:damarualigali 1882 Lo mong:ubadamaaligali 1883 Lo mong:ubadamaaligaliinverted 1884 Lo mong:baludaaligali 1885 Mn mong:baludaaligalithree 1886 Mn mong:aaligali 1887 Lo mong:ialigali 1888 Lo mong:kaaligali 1889 Lo mong:ngaaligali 188A Lo mong:caaligali 188B Lo mong:ttaaligali 188C Lo mong:tthaaligali 188D Lo mong:ddaaligali 188E Lo mong:nnaaligali 188F Lo mong:taaligali 1890 Lo mong:daaligali 1891 Lo mong:paaligali 1892 Lo mong:phaaligali 1893 Lo mong:ssaaligali 1894 Lo mong:zhaaligali 1895 Lo mong:zaaligali 1896 Lo mong:ahaligali 1897 Lo mong:tatodoaligali 1898 Lo mong:zhatodoaligali 1899 Lo mong:ghamanchualigali 189A Lo mong:ngamanchualigali 189B Lo mong:camanchualigali 189C Lo mong:jhamanchualigali 189D Lo mong:ttamanchualigali 189E Lo mong:ddhamanchualigali 189F Lo mong:tamanchualigali 18A0 Lo mong:dhamanchualigali 18A1 Lo mong:ssamanchualigali 18A2 Lo mong:cyamanchualigali 18A3 Lo mong:zhamanchualigali 18A4 Lo mong:zamanchualigali 18A5 Lo mong:ualigalihalf 18A6 Lo mong:yaaligalihalf 18A7 Lo mong:bhamanchualigali 18A8 Lo mong:dagalgaaligali 18A9 Mn mong:lhamanchualigali 18AA Lo # Georgian Extended AnGeor 1C90 Lu BanGeor 1C91 Lu GanGeor 1C92 Lu DonGeor 1C93 Lu EnGeor 1C94 Lu VinGeor 1C95 Lu ZenGeor 1C96 Lu TanGeor 1C97 Lu InGeor 1C98 Lu KanGeor 1C99 Lu LasGeor 1C9A Lu ManGeor 1C9B Lu NarGeor 1C9C Lu OnGeor 1C9D Lu ParGeor 1C9E Lu ZharGeor 1C9F Lu RaeGeor 1CA0 Lu SanGeor 1CA1 Lu TarGeor 1CA2 Lu UnGeor 1CA3 Lu PharGeor 1CA4 Lu KharGeor 1CA5 Lu GhanGeor 1CA6 Lu QarGeor 1CA7 Lu ShinGeor 1CA8 Lu ChinGeor 1CA9 Lu CanGeor 1CAA Lu JilGeor 1CAB Lu CilGeor 1CAC Lu CharGeor 1CAD Lu XanGeor 1CAE Lu JhanGeor 1CAF Lu HaeGeor 1CB0 Lu HeGeor 1CB1 Lu HieGeor 1CB2 Lu WeGeor 1CB3 Lu HarGeor 1CB4 Lu HoeGeor 1CB5 Lu FiGeor 1CB6 Lu YnGeor 1CB7 Lu ElifiGeor 1CB8 Lu TurnedganGeor 1CB9 Lu AinGeor 1CBA Lu AenGeor 1CBD Lu HardsignGeor 1CBE Lu LabialsignGeor 1CBF Lu # Vedic Extensions ve:karshanatone 1CD0 Mn ve:sharatone 1CD1 Mn ve:prenkhatone 1CD2 Mn ve:nihshvasasign 1CD3 Po ve:yajurmidlinesvaritasign 1CD4 Mn ve:yajuraggravatedindependentsvaritatone 1CD5 Mn ve:yajurindependentsvaritatone 1CD6 Mn ve:yajurkathakaindependentsvaritatone 1CD7 Mn ve:belowtonecandra 1CD8 Mn ve:yajurkathakaindependentsvaritaschroedertone 1CD9 Mn ve:svaritatonedbl 1CDA Mn ve:svaritatonetpl 1CDB Mn ve:kathakaanudattatone 1CDC Mn ve:dotbelowtone 1CDD Mn ve:twodotsbelowtone 1CDE Mn ve:threedotsbelowtone 1CDF Mn ve:rigkashmiriindependentsvaritatone 1CE0 Mn ve:atharvaindependentsvaritatone 1CE1 Mc ve:visargasvaritasign 1CE2 Mn ve:visargaudattasign 1CE3 Mn ve:visargaudattasignreversed 1CE4 Mn ve:visargaanudattasign 1CE5 Mn ve:visargaanudattasignreversed 1CE6 Mn ve:visargaudattawithtailsign 1CE7 Mn ve:visargaanudattawithtailsign 1CE8 Mn ve:anusvaraantargomukhasign 1CE9 Lo ve:anusvarabahirgomukhasign 1CEA Lo ve:anusvaravamagomukhasign 1CEB Lo ve:anusvaravamagomukhawithtailsign 1CEC Lo ve:tiryaksign 1CED Mn ve:hexiformanusvarasignlong 1CEE Lo ve:anusvarasignlong 1CEF Lo ve:rthanganusvarasignlong 1CF0 Lo ve:anusvaraubhayatomukhasign 1CF1 Lo ve:ardhavisargasign 1CF2 Lo ve:rotatedardhavisargasign 1CF3 Lo ve:abovetonecandra 1CF4 Mn ve:jihvamuliyasign 1CF5 Lo ve:upadhmaniyasign 1CF6 Lo ve:atikramasign 1CF7 Mc ve:ringabovetone 1CF8 Mn ve:ringabovetonedbl 1CF9 Mn ve:anusvaraantargomukhasigndbl 1CFA Lo # Phonetic Extensions Asmall 1D00 Ll AEsmall 1D01 Ll aeturned 1D02 Ll Bbarsmall 1D03 Ll Csmall 1D04 Ll Dsmall 1D05 Ll Ethsmall 1D06 Ll Esmall 1D07 Ll eopenturned 1D08 Ll iturned 1D09 Ll Jsmall 1D0A Ll Ksmall 1D0B Ll Lsmallstroke 1D0C Ll Msmall 1D0D Ll Nsmallreversed 1D0E Ll Osmall 1D0F Ll Oopensmall 1D10 Ll osideways 1D11 Ll oopensideways 1D12 Ll ostrokesideways 1D13 Ll oeturned 1D14 Ll OUsmall 1D15 Ll otophalf 1D16 Ll obottomhalf 1D17 Ll Psmall 1D18 Ll Rsmallreversed 1D19 Ll Rsmallturned 1D1A Ll Tsmall 1D1B Ll Usmall 1D1C Ll usideways 1D1D Ll usidewaysdieresised 1D1E Ll mturnedsideways 1D1F Ll Vsmall 1D20 Ll Wsmall 1D21 Ll Zsmall 1D22 Ll Ezhsmall 1D23 Ll spirantvoicedlaryngeal 1D24 Ll phon:ain 1D25 Ll gr:Gammasmall 1D26 Ll gr:Lambdasmall 1D27 Ll gr:Pismall 1D28 Ll gr:RsmallHO 1D29 Ll gr:Psismall 1D2A Ll Elsmallcyr 1D2B Ll Amod 1D2C Lm Aemod 1D2D Lm Bmod 1D2E Lm Bbarmod 1D2F Lm Dmod 1D30 Lm Emod 1D31 Lm Ereversedmod 1D32 Lm Gmod 1D33 Lm Hmod 1D34 Lm Imod 1D35 Lm Jmod 1D36 Lm Kmod 1D37 Lm Lmod 1D38 Lm Mmod 1D39 Lm Nmod 1D3A Lm Nreversedmod 1D3B Lm Omod 1D3C Lm Oumod 1D3D Lm Pmod 1D3E Lm Rmod 1D3F Lm Tmod 1D40 Lm Umod 1D41 Lm Wmod 1D42 Lm amod 1D43 Lm aturnedmod 1D44 Lm alphamod 1D45 Lm aeturnedmod 1D46 Lm bmod 1D47 Lm dmod 1D48 Lm emod 1D49 Lm schwamod 1D4A Lm eopenmod 1D4B Lm eopenturnedmod 1D4C Lm gmod 1D4D Lm iturnedmod 1D4E Lm kmod 1D4F Lm mmod 1D50 Lm engmod 1D51 Lm omod 1D52 Lm oopenmod 1D53 Lm otophalfmod 1D54 Lm obottomhalfmod 1D55 Lm pmod 1D56 Lm tmod 1D57 Lm umod 1D58 Lm usidewaysmod 1D59 Lm mturnedmod 1D5A Lm vmod 1D5B Lm ainmod 1D5C Lm betamod 1D5D Lm gr:gammamod 1D5E Lm deltamod 1D5F Lm gr:phimod 1D60 Lm chimod 1D61 Lm isubscript 1D62 Lm rsubscript 1D63 Lm usubscript 1D64 Lm vsubscript 1D65 Lm gr:betasubscript 1D66 Lm gr:gammasubscript 1D67 Lm gr:rhosubscript 1D68 Lm gr:phisubscript 1D69 Lm gr:chisubscript 1D6A Lm ue 1D6B Ll bmiddletilde 1D6C Ll dmiddletilde 1D6D Ll fmiddletilde 1D6E Ll mmiddletilde 1D6F Ll nmiddletilde 1D70 Ll pmiddletilde 1D71 Ll rmiddletilde 1D72 Ll rfishmiddletilde 1D73 Ll smiddletilde 1D74 Ll tmiddletilde 1D75 Ll zmiddletilde 1D76 Ll gturned 1D77 Ll ENcyrmod 1D78 Lm ginsular 1D79 Ll thstrike 1D7A Ll Ismallstroke 1D7B Ll iotastroke 1D7C Ll pstroke 1D7D Ll Usmallstroke 1D7E Ll upsilonstroke 1D7F Ll # Phonetic Extensions Supplement bpalatalhook 1D80 Ll dpalatalhook 1D81 Ll fpalatalhook 1D82 Ll gpalatalhook 1D83 Ll kpalatalhook 1D84 Ll lpalatalhook 1D85 Ll mpalatalhook 1D86 Ll npalatalhook 1D87 Ll ppalatalhook 1D88 Ll rpalatalhook 1D89 Ll spalatalhook 1D8A Ll eshpalatalhook 1D8B Ll vpalatalhook 1D8C Ll xpalatalhook 1D8D Ll zpalatalhook 1D8E Ll aretroflexhook 1D8F Ll alpharetroflexhook 1D90 Ll dhooktail 1D91 Ll eretroflexhook 1D92 Ll eopenretroflexhook 1D93 Ll eopenreversedretroflexhook 1D94 Ll schwaretroflexhook 1D95 Ll iretroflexhook 1D96 Ll oopenretroflexhook 1D97 Ll eshretroflexhook 1D98 Ll uretroflexhook 1D99 Ll ezhretroflexhook 1D9A Ll alphaturnedmod 1D9B Lm cmod 1D9C Lm ccurlmod 1D9D Lm ethmod 1D9E Lm eopenreversedmod 1D9F Lm fmod 1DA0 Lm dotlessjstrokemod 1DA1 Lm gscriptmod 1DA2 Lm hturnedmod 1DA3 Lm istrokemod 1DA4 Lm iotamod 1DA5 Lm Ismallmod 1DA6 Lm Istrokesmallmod 1DA7 Lm jcrossedtailmod 1DA8 Lm lretroflexhookmod 1DA9 Lm lpalatalhookmod 1DAA Lm Lsmallmod 1DAB Lm mhookmod 1DAC Lm mlonglegturnedmod 1DAD Lm nlefthookmod 1DAE Lm nretroflexhookmod 1DAF Lm Nsmallmod 1DB0 Lm obarmod 1DB1 Lm phimod 1DB2 Lm shookmod 1DB3 Lm eshmod 1DB4 Lm tpalatalhookmod 1DB5 Lm ubarmod 1DB6 Lm upsilonmod 1DB7 Lm Usmallmod 1DB8 Lm vhookmod 1DB9 Lm vturnedmod 1DBA Lm zmod 1DBB Lm zretroflexhookmod 1DBC Lm zcurlmod 1DBD Lm ezhmod 1DBE Lm thetamod 1DBF Lm # Combining Diacritical Marks Supplement dottedgravecmb 1DC0 Mn dottedacutecmb 1DC1 Mn snakebelowcmb 1DC2 Mn suspensionmarkcmb 1DC3 Mn macronacutecmb 1DC4 Mn gravemacroncmb 1DC5 Mn macrongravecmb 1DC6 Mn acutemacroncmb 1DC7 Mn graveacutegravecmb 1DC8 Mn acutegraveacutecmb 1DC9 Mn rbelowcmb 1DCA Mn brevemacroncmb 1DCB Mn macronbrevecmb 1DCC Mn doubleabovecircumflexcmb 1DCD Mn aboveogonekcmb 1DCE Mn zigzagbelowcmb 1DCF Mn isbelowcmb 1DD0 Mn urabovecmb 1DD1 Mn usabovecmb 1DD2 Mn aaboveflatcmb 1DD3 Mn aecmb 1DD4 Mn aocmb 1DD5 Mn avcmb 1DD6 Mn ccedillacmb 1DD7 Mn insulardcmb 1DD8 Mn ethcmb 1DD9 Mn gcmb 1DDA Mn gsmallcmb 1DDB Mn kcmb 1DDC Mn lcmb 1DDD Mn lsmallcmb 1DDE Mn msmallcmb 1DDF Mn ncmb 1DE0 Mn nsmallcmb 1DE1 Mn rsmallcmb 1DE2 Mn rrotundacmb 1DE3 Mn scmb 1DE4 Mn longscmb 1DE5 Mn zcmb 1DE6 Mn alphacmb 1DE7 Mn bcmb 1DE8 Mn betacmb 1DE9 Mn schwacmb 1DEA Mn fcmb 1DEB Mn lwithdoublemiddletildecmb 1DEC Mn owithlightcentralizationstrokecmb 1DED Mn pcmb 1DEE Mn eshcmb 1DEF Mn uwithlightcentralizationstrokecmb 1DF0 Mn wcmb 1DF1 Mn adieresiscmb 1DF2 Mn odieresiscmb 1DF3 Mn udieresiscmb 1DF4 Mn uptackabovecmb 1DF5 Mn kavykaaboverightcmb 1DF6 Mn kavykaaboveleftcmb 1DF7 Mn dotaboveleftcmb 1DF8 Mn wideinvertedbridgebelowcmb 1DF9 Mn deletionmarkcmb 1DFB Mn doubleinvertedbelowbrevecmb 1DFC Mn almostequaltobelowcmb 1DFD Mn leftarrowheadabovecmb 1DFE Mn rightarrowheadanddownarrowheadbelowcmb 1DFF Mn # Latin Extended Additional Aringbelow 1E00 Lu aringbelow 1E01 Ll Bdot 1E02 Lu bdot 1E03 Ll Bdotbelow 1E04 Lu bdotbelow 1E05 Ll Blinebelow 1E06 Lu blinebelow 1E07 Ll Ccedillaacute 1E08 Lu ccedillaacute 1E09 Ll Ddot 1E0A Lu ddot 1E0B Ll Ddotbelow 1E0C Lu ddotbelow 1E0D Ll Dlinebelow 1E0E Lu dlinebelow 1E0F Ll Dcedilla 1E10 Lu dcedilla 1E11 Ll Dcircumflexbelow 1E12 Lu dcircumflexbelow 1E13 Ll Emacrongrave 1E14 Lu emacrongrave 1E15 Ll Emacronacute 1E16 Lu emacronacute 1E17 Ll Ecircumflexbelow 1E18 Lu ecircumflexbelow 1E19 Ll Etildebelow 1E1A Lu etildebelow 1E1B Ll Ecedillabreve 1E1C Lu ecedillabreve 1E1D Ll Fdot 1E1E Lu fdot 1E1F Ll Gmacron 1E20 Lu gmacron 1E21 Ll Hdot 1E22 Lu hdot 1E23 Ll Hdotbelow 1E24 Lu hdotbelow 1E25 Ll Hdieresis 1E26 Lu hdieresis 1E27 Ll Hcedilla 1E28 Lu hcedilla 1E29 Ll Hbrevebelow 1E2A Lu hbrevebelow 1E2B Ll Itildebelow 1E2C Lu itildebelow 1E2D Ll Idieresisacute 1E2E Lu idieresisacute 1E2F Ll Kacute 1E30 Lu kacute 1E31 Ll Kdotbelow 1E32 Lu kdotbelow 1E33 Ll Klinebelow 1E34 Lu klinebelow 1E35 Ll Ldotbelow 1E36 Lu ldotbelow 1E37 Ll Lmacrondot 1E38 Lu lmacrondot 1E39 Ll Llinebelow 1E3A Lu llinebelow 1E3B Ll Lcircumflexbelow 1E3C Lu lcircumflexbelow 1E3D Ll Macute 1E3E Lu macute 1E3F Ll Mdot 1E40 Lu mdot 1E41 Ll Mdotbelow 1E42 Lu mdotbelow 1E43 Ll Ndot 1E44 Lu ndot 1E45 Ll Ndotbelow 1E46 Lu ndotbelow 1E47 Ll Nlinebelow 1E48 Lu nlinebelow 1E49 Ll Ncircumflexbelow 1E4A Lu ncircumflexbelow 1E4B Ll Otildeacute 1E4C Lu otildeacute 1E4D Ll Otildedieresis 1E4E Lu otildedieresis 1E4F Ll Omacrongrave 1E50 Lu omacrongrave 1E51 Ll Omacronacute 1E52 Lu omacronacute 1E53 Ll Pacute 1E54 Lu pacute 1E55 Ll Pdot 1E56 Lu pdot 1E57 Ll Rdot 1E58 Lu rdot 1E59 Ll Rdotbelow 1E5A Lu rdotbelow 1E5B Ll Rmacrondot 1E5C Lu rmacrondot 1E5D Ll Rlinebelow 1E5E Lu rlinebelow 1E5F Ll Sdot 1E60 Lu sdot 1E61 Ll Sdotbelow 1E62 Lu sdotbelow 1E63 Ll Sacutedotaccent 1E64 Lu sacutedotaccent 1E65 Ll Scarondot 1E66 Lu scarondot 1E67 Ll Sdotbelowdotabove 1E68 Lu sdotbelowdotabove 1E69 Ll Tdot 1E6A Lu tdot 1E6B Ll Tdotbelow 1E6C Lu tdotbelow 1E6D Ll Tlinebelow 1E6E Lu tlinebelow 1E6F Ll Tcircumflexbelow 1E70 Lu tcircumflexbelow 1E71 Ll Udieresisbelow 1E72 Lu udieresisbelow 1E73 Ll Utildebelow 1E74 Lu utildebelow 1E75 Ll Ucircumflexbelow 1E76 Lu ucircumflexbelow 1E77 Ll Utildeacute 1E78 Lu utildeacute 1E79 Ll Umacrondieresis 1E7A Lu umacrondieresis 1E7B Ll Vtilde 1E7C Lu vtilde 1E7D Ll Vdotbelow 1E7E Lu vdotbelow 1E7F Ll Wgrave 1E80 Lu wgrave 1E81 Ll Wacute 1E82 Lu wacute 1E83 Ll Wdieresis 1E84 Lu wdieresis 1E85 Ll Wdot 1E86 Lu wdot 1E87 Ll Wdotbelow 1E88 Lu wdotbelow 1E89 Ll Xdot 1E8A Lu xdot 1E8B Ll Xdieresis 1E8C Lu xdieresis 1E8D Ll Ydot 1E8E Lu ydot 1E8F Ll Zcircumflex 1E90 Lu zcircumflex 1E91 Ll Zdotbelow 1E92 Lu zdotbelow 1E93 Ll Zlinebelow 1E94 Lu zlinebelow 1E95 Ll hlinebelow 1E96 Ll tdieresis 1E97 Ll wring 1E98 Ll yring 1E99 Ll arighthalfring 1E9A Ll longsdot 1E9B Ll longswithdiagonalstroke 1E9C Ll longswithhighstroke 1E9D Ll Germandbls 1E9E Lu lt:delta 1E9F Ll Adotbelow 1EA0 Lu adotbelow 1EA1 Ll Ahoi 1EA2 Lu ahoi 1EA3 Ll Acircumflexacute 1EA4 Lu acircumflexacute 1EA5 Ll Acircumflexgrave 1EA6 Lu acircumflexgrave 1EA7 Ll Acircumflexhoi 1EA8 Lu acircumflexhoi 1EA9 Ll Acircumflextilde 1EAA Lu acircumflextilde 1EAB Ll Acircumflexdotbelow 1EAC Lu acircumflexdotbelow 1EAD Ll Abreveacute 1EAE Lu abreveacute 1EAF Ll Abrevegrave 1EB0 Lu abrevegrave 1EB1 Ll Abrevehoi 1EB2 Lu abrevehoi 1EB3 Ll Abrevetilde 1EB4 Lu abrevetilde 1EB5 Ll Abrevedotbelow 1EB6 Lu abrevedotbelow 1EB7 Ll Edotbelow 1EB8 Lu edotbelow 1EB9 Ll Ehoi 1EBA Lu ehoi 1EBB Ll Etilde 1EBC Lu etilde 1EBD Ll Ecircumflexacute 1EBE Lu ecircumflexacute 1EBF Ll Ecircumflexgrave 1EC0 Lu ecircumflexgrave 1EC1 Ll Ecircumflexhoi 1EC2 Lu ecircumflexhoi 1EC3 Ll Ecircumflextilde 1EC4 Lu ecircumflextilde 1EC5 Ll Ecircumflexdotbelow 1EC6 Lu ecircumflexdotbelow 1EC7 Ll Ihoi 1EC8 Lu ihoi 1EC9 Ll Idotbelow 1ECA Lu idotbelow 1ECB Ll Odotbelow 1ECC Lu odotbelow 1ECD Ll Ohoi 1ECE Lu ohoi 1ECF Ll Ocircumflexacute 1ED0 Lu ocircumflexacute 1ED1 Ll Ocircumflexgrave 1ED2 Lu ocircumflexgrave 1ED3 Ll Ocircumflexhoi 1ED4 Lu ocircumflexhoi 1ED5 Ll Ocircumflextilde 1ED6 Lu ocircumflextilde 1ED7 Ll Ocircumflexdotbelow 1ED8 Lu ocircumflexdotbelow 1ED9 Ll Ohornacute 1EDA Lu ohornacute 1EDB Ll Ohorngrave 1EDC Lu ohorngrave 1EDD Ll Ohornhoi 1EDE Lu ohornhoi 1EDF Ll Ohorntilde 1EE0 Lu ohorntilde 1EE1 Ll Ohorndotbelow 1EE2 Lu ohorndotbelow 1EE3 Ll Udotbelow 1EE4 Lu udotbelow 1EE5 Ll Uhoi 1EE6 Lu uhoi 1EE7 Ll Uhornacute 1EE8 Lu uhornacute 1EE9 Ll Uhorngrave 1EEA Lu uhorngrave 1EEB Ll Uhornhoi 1EEC Lu uhornhoi 1EED Ll Uhorntilde 1EEE Lu uhorntilde 1EEF Ll Uhorndotbelow 1EF0 Lu uhorndotbelow 1EF1 Ll Ygrave 1EF2 Lu ygrave 1EF3 Ll Ydotbelow 1EF4 Lu ydotbelow 1EF5 Ll Yhoi 1EF6 Lu yhoi 1EF7 Ll Ytilde 1EF8 Lu ytilde 1EF9 Ll LLwelsh 1EFA Lu llwelsh 1EFB Ll Vwelsh 1EFC Lu vwelsh 1EFD Ll Yloop 1EFE Lu yloop 1EFF Ll # Greek Extended alphalenis 1F00 Ll alphaasper 1F01 Ll alphalenisgrave 1F02 Ll alphaaspergrave 1F03 Ll alphalenisacute 1F04 Ll alphaasperacute 1F05 Ll alphalenistilde 1F06 Ll alphaaspertilde 1F07 Ll Alphalenis 1F08 Lu Alphaasper 1F09 Lu Alphalenisgrave 1F0A Lu Alphaaspergrave 1F0B Lu Alphalenisacute 1F0C Lu Alphaasperacute 1F0D Lu Alphalenistilde 1F0E Lu Alphaaspertilde 1F0F Lu epsilonlenis 1F10 Ll epsilonasper 1F11 Ll epsilonlenisgrave 1F12 Ll epsilonaspergrave 1F13 Ll epsilonlenisacute 1F14 Ll epsilonasperacute 1F15 Ll Epsilonlenis 1F18 Lu Epsilonasper 1F19 Lu Epsilonlenisgrave 1F1A Lu Epsilonaspergrave 1F1B Lu Epsilonlenisacute 1F1C Lu Epsilonasperacute 1F1D Lu etalenis 1F20 Ll etaasper 1F21 Ll etalenisgrave 1F22 Ll etaaspergrave 1F23 Ll etalenisacute 1F24 Ll etaasperacute 1F25 Ll etalenistilde 1F26 Ll etaaspertilde 1F27 Ll Etalenis 1F28 Lu Etaasper 1F29 Lu Etalenisgrave 1F2A Lu Etaaspergrave 1F2B Lu Etalenisacute 1F2C Lu Etaasperacute 1F2D Lu Etalenistilde 1F2E Lu Etaaspertilde 1F2F Lu iotalenis 1F30 Ll iotaasper 1F31 Ll iotalenisgrave 1F32 Ll iotaaspergrave 1F33 Ll iotalenisacute 1F34 Ll iotaasperacute 1F35 Ll iotalenistilde 1F36 Ll iotaaspertilde 1F37 Ll Iotalenis 1F38 Lu Iotaasper 1F39 Lu Iotalenisgrave 1F3A Lu Iotaaspergrave 1F3B Lu Iotalenisacute 1F3C Lu Iotaasperacute 1F3D Lu Iotalenistilde 1F3E Lu Iotaaspertilde 1F3F Lu omicronlenis 1F40 Ll omicronasper 1F41 Ll omicronlenisgrave 1F42 Ll omicronaspergrave 1F43 Ll omicronlenisacute 1F44 Ll omicronasperacute 1F45 Ll Omicronlenis 1F48 Lu Omicronasper 1F49 Lu Omicronlenisgrave 1F4A Lu Omicronaspergrave 1F4B Lu Omicronlenisacute 1F4C Lu Omicronasperacute 1F4D Lu upsilonlenis 1F50 Ll upsilonasper 1F51 Ll upsilonlenisgrave 1F52 Ll upsilonaspergrave 1F53 Ll upsilonlenisacute 1F54 Ll upsilonasperacute 1F55 Ll upsilonlenistilde 1F56 Ll upsilonaspertilde 1F57 Ll Upsilonasper 1F59 Lu Upsilonaspergrave 1F5B Lu Upsilonasperacute 1F5D Lu Upsilonaspertilde 1F5F Lu omegalenis 1F60 Ll omegaasper 1F61 Ll omegalenisgrave 1F62 Ll omegaaspergrave 1F63 Ll omegalenisacute 1F64 Ll omegaasperacute 1F65 Ll omegalenistilde 1F66 Ll omegaaspertilde 1F67 Ll Omegalenis 1F68 Lu Omegaasper 1F69 Lu Omegalenisgrave 1F6A Lu Omegaaspergrave 1F6B Lu Omegalenisacute 1F6C Lu Omegaasperacute 1F6D Lu Omegalenistilde 1F6E Lu Omegaaspertilde 1F6F Lu alphagrave 1F70 Ll alphaacute 1F71 Ll epsilongrave 1F72 Ll epsilonacute 1F73 Ll etagrave 1F74 Ll etaacute 1F75 Ll iotagrave 1F76 Ll iotaacute 1F77 Ll omicrongrave 1F78 Ll omicronacute 1F79 Ll upsilongrave 1F7A Ll upsilonacute 1F7B Ll omegagrave 1F7C Ll omegaacute 1F7D Ll alphalenisiotasub 1F80 Ll alphaasperiotasub 1F81 Ll alphalenisgraveiotasub 1F82 Ll alphaaspergraveiotasub 1F83 Ll alphalenisacuteiotasub 1F84 Ll alphaasperacuteiotasub 1F85 Ll alphalenistildeiotasub 1F86 Ll alphaaspertildeiotasub 1F87 Ll Alphalenisiotasub 1F88 Lt Alphaasperiotasub 1F89 Lt Alphalenisgraveiotasub 1F8A Lt Alphaaspergraveiotasub 1F8B Lt Alphalenisacuteiotasub 1F8C Lt Alphaasperacuteiotasub 1F8D Lt Alphalenistildeiotasub 1F8E Lt Alphaaspertildeiotasub 1F8F Lt etalenisiotasub 1F90 Ll etaasperiotasub 1F91 Ll etalenisgraveiotasub 1F92 Ll etaaspergraveiotasub 1F93 Ll etalenisacuteiotasub 1F94 Ll etaasperacuteiotasub 1F95 Ll etalenistildeiotasub 1F96 Ll etaaspertildeiotasub 1F97 Ll Etalenisiotasub 1F98 Lt Etaasperiotasub 1F99 Lt Etalenisgraveiotasub 1F9A Lt Etaaspergraveiotasub 1F9B Lt Etalenisacuteiotasub 1F9C Lt Etaasperacuteiotasub 1F9D Lt Etalenistildeiotasub 1F9E Lt Etaaspertildeiotasub 1F9F Lt omegalenisiotasub 1FA0 Ll omegaasperiotasub 1FA1 Ll omegalenisgraveiotasub 1FA2 Ll omegaaspergraveiotasub 1FA3 Ll omegalenisacuteiotasub 1FA4 Ll omegaasperacuteiotasub 1FA5 Ll omegalenistildeiotasub 1FA6 Ll omegaaspertildeiotasub 1FA7 Ll Omegalenisiotasub 1FA8 Lt Omegaasperiotasub 1FA9 Lt Omegalenisgraveiotasub 1FAA Lt Omegaaspergraveiotasub 1FAB Lt Omegalenisacuteiotasub 1FAC Lt Omegaasperacuteiotasub 1FAD Lt Omegalenistildeiotasub 1FAE Lt Omegaaspertildeiotasub 1FAF Lt alphabreve 1FB0 Ll alphawithmacron 1FB1 Ll alphagraveiotasub 1FB2 Ll alphaiotasub 1FB3 Ll alphaacuteiotasub 1FB4 Ll alphatilde 1FB6 Ll alphatildeiotasub 1FB7 Ll Alphabreve 1FB8 Lu Alphawithmacron 1FB9 Lu Alphagrave 1FBA Lu Alphaacute 1FBB Lu Alphaiotasub 1FBC Lt KORONIS 1FBD Sk iotaadscript 1FBE Ll lenis 1FBF Sk gr:tilde 1FC0 Sk dieresistilde 1FC1 Sk etagraveiotasub 1FC2 Ll etaiotasub 1FC3 Ll etaacuteiotasub 1FC4 Ll etatilde 1FC6 Ll etatildeiotasub 1FC7 Ll Epsilongrave 1FC8 Lu Epsilonacute 1FC9 Lu Etagrave 1FCA Lu Etaacute 1FCB Lu Etaiotasub 1FCC Lt lenisgrave 1FCD Sk lenisacute 1FCE Sk lenistilde 1FCF Sk iotabreve 1FD0 Ll iotawithmacron 1FD1 Ll iotadieresisgrave 1FD2 Ll iotadieresisacute 1FD3 Ll iotatilde 1FD6 Ll iotadieresistilde 1FD7 Ll Iotabreve 1FD8 Lu Iotawithmacron 1FD9 Lu Iotagrave 1FDA Lu Iotaacute 1FDB Lu aspergrave 1FDD Sk asperacute 1FDE Sk aspertilde 1FDF Sk upsilonbreve 1FE0 Ll upsilonwithmacron 1FE1 Ll upsilondieresisgrave 1FE2 Ll upsilondieresisacute 1FE3 Ll rholenis 1FE4 Ll rhoasper 1FE5 Ll upsilontilde 1FE6 Ll upsilondieresistilde 1FE7 Ll Upsilonbreve 1FE8 Lu Upsilonwithmacron 1FE9 Lu Upsilongrave 1FEA Lu Upsilonacute 1FEB Lu Rhoasper 1FEC Lu dieresisgrave 1FED Sk dieresisacute 1FEE Sk gr:grave 1FEF Sk omegagraveiotasub 1FF2 Ll omegaiotasub 1FF3 Ll omegaacuteiotasub 1FF4 Ll omegatilde 1FF6 Ll omegatildeiotasub 1FF7 Ll Omicrongrave 1FF8 Lu Omicronacute 1FF9 Lu Omegagrave 1FFA Lu Omegaacute 1FFB Lu Omegaiotasub 1FFC Lt gr:acute 1FFD Sk asper 1FFE Sk # General Punctuation enquad 2000 Zs emquad 2001 Zs enspace 2002 Zs emspace 2003 Zs threeperemspace 2004 Zs fourperemspace 2005 Zs sixperemspace 2006 Zs figurespace 2007 Zs punctuationspace 2008 Zs thinspace 2009 Zs hairspace 200A Zs zerowidthspace 200B Cf zerowidthnonjoiner 200C Cf zerowidthjoiner 200D Cf lefttorightmark 200E Cf righttoleftmark 200F Cf gnrl:hyphen 2010 Pd nonbreakinghyphen 2011 Pd figuredash 2012 Pd endash 2013 Pd emdash 2014 Pd horizontalbar 2015 Pd verticalbardbl 2016 Po underscoredbl 2017 Po quoteleft 2018 Pi quoteright 2019 Pf quotesinglbase 201A Ps quotereversed 201B Pi quotedblleft 201C Pi quotedblright 201D Pf quotedblbase 201E Ps quotedblreversed 201F Pi dagger 2020 Po daggerdbl 2021 Po bullet 2022 Po triangularbullet 2023 Po onedotenleader 2024 Po twodotenleader 2025 Po ellipsis 2026 Po hyphenationpoint 2027 Po lineseparator 2028 Zl paragraphseparator 2029 Zp lefttorightembed 202A Cf righttoleftembed 202B Cf popdirectionalformatting 202C Cf lefttorightoverride 202D Cf righttoleftoverride 202E Cf narrownobreakspace 202F Zs perthousand 2030 Po pertenthousandsign 2031 Po minute 2032 Po second 2033 Po millisecond 2034 Po minutereversed 2035 Po secondreversed 2036 Po millisecondreversed 2037 Po caret 2038 Po guilsinglleft 2039 Pi guilsinglright 203A Pf referencemark 203B Po exclamdbl 203C Po interrobang 203D Po overline 203E Po undertie 203F Pc charactertie 2040 Pc caretinsertionpoint 2041 Po asterism 2042 Po hyphenbullet 2043 Po fraction 2044 Sm bracketleftsquarequill 2045 Ps bracketrightsquarequill 2046 Pe questiondbl 2047 Po questionexclamationmark 2048 Po exclamationquestion 2049 Po tironiansignet 204A Po pilcrowsignreversed 204B Po blackwardsbulletleft 204C Po blackwardsbulletright 204D Po lowasterisk 204E Po semicolonreversed 204F Po closeup 2050 Po twoasterisksalignedvertically 2051 Po commercialminussign 2052 Sm swungdash 2053 Po invertedundertie 2054 Pc flowerpunctuationmark 2055 Po threedotpunctuation 2056 Po quadrupleminute 2057 Po fourdotpunctuation 2058 Po fivedotpunctuation 2059 Po twodotpunctuation 205A Po fourdotmark 205B Po dottedcross 205C Po tricolon 205D Po verticalfourdots 205E Po mediummathematicalspace 205F Zs wordjoiner 2060 Cf functionapplication 2061 Cf invisibletimes 2062 Cf invisibleseparator 2063 Cf invisibleplus 2064 Cf lefttorightisolate 2066 Cf righttoleftisolate 2067 Cf firststrongisolate 2068 Cf popdirectionalisolate 2069 Cf inhibitsymmetricswapping 206A Cf activatesymmetricswapping 206B Cf inhibitarabicformshaping 206C Cf activatearabicformshaping 206D Cf nationaldigitshapes 206E Cf nominaldigitshapes 206F Cf # Superscripts and Subscripts zero.superior 2070 No i.superior 2071 Lm four.superior 2074 No five.superior 2075 No six.superior 2076 No seven.superior 2077 No eight.superior 2078 No nine.superior 2079 No plus.superior 207A Sm minus.superior 207B Sm equal.superior 207C Sm parenleft.superior 207D Ps parenright.superior 207E Pe n.superior 207F Lm zero.inferior 2080 No one.inferior 2081 No two.inferior 2082 No three.inferior 2083 No four.inferior 2084 No five.inferior 2085 No six.inferior 2086 No seven.inferior 2087 No eight.inferior 2088 No nine.inferior 2089 No plus.inferior 208A Sm minus.inferior 208B Sm equal.inferior 208C Sm parenleft.inferior 208D Ps parenright.inferior 208E Pe a.inferior 2090 Lm e.inferior 2091 Lm o.inferior 2092 Lm x.inferior 2093 Lm schwa.inferior 2094 Lm h.inferior 2095 Lm k.inferior 2096 Lm l.inferior 2097 Lm m.inferior 2098 Lm n.inferior 2099 Lm p.inferior 209A Lm s.inferior 209B Lm t.inferior 209C Lm # Currency Symbols euroarchaic 20A0 Sc colonmonetary 20A1 Sc cruzeiro 20A2 Sc franc 20A3 Sc lira 20A4 Sc mill 20A5 Sc naira 20A6 Sc peseta 20A7 Sc rupee 20A8 Sc won 20A9 Sc newsheqel 20AA Sc dong 20AB Sc Euro 20AC Sc kip 20AD Sc tugrik 20AE Sc drachma 20AF Sc germanpenny 20B0 Sc peso 20B1 Sc guarani 20B2 Sc austral 20B3 Sc hryvnia 20B4 Sc cedi 20B5 Sc livretournois 20B6 Sc spesmilo 20B7 Sc tenge 20B8 Sc indianrupee 20B9 Sc turkishlira 20BA Sc nordicmark 20BB Sc manat 20BC Sc ruble 20BD Sc lari 20BE Sc bitcoin 20BF Sc # Letterlike Symbols accountof 2100 So addressedsubject 2101 So Cdblstruck 2102 Lu degreecelsius 2103 So centreline 2104 So careof 2105 So cadauna 2106 So euler 2107 Lu scruple 2108 So degreefahrenheit 2109 So lttr:gscript 210A Ll Hscript 210B Lu Hfraktur 210C Lu Hdblstruck 210D Lu planck 210E Ll plancktwopi 210F Ll Iscript 2110 Lu Ifraktur 2111 Lu Lscript 2112 Lu litre 2113 Ll lbbar 2114 So Ndblstruck 2115 Lu numero 2116 So soundcopyright 2117 So weierstrass 2118 Sm Pdblstruck 2119 Lu Qdblstruck 211A Lu Rscript 211B Lu Rfraktur 211C Lu Rdblstruck 211D Lu prescription 211E So response 211F So servicemark 2120 So telephone 2121 So trademark 2122 So versicle 2123 So Zdblstruck 2124 Lu ounce 2125 So ohm 2126 Lu ohminverted 2127 So Zfraktur 2128 Lu iotaturned 2129 So kelvin 212A Lu angstrom 212B Lu Bscript 212C Lu Cfraktur 212D Lu estimated 212E So escript 212F Ll Escript 2130 Lu Fscript 2131 Lu Fturned 2132 Lu Mscript 2133 Lu oscript 2134 Ll aleph 2135 Lo bet 2136 Lo gimel 2137 Lo dalet 2138 Lo information 2139 Ll Qrotated 213A So facsimile 213B So pidblstruck 213C Ll gammadblstruck 213D Ll Gammadblstruck 213E Lu Pidblstruck 213F Lu summationdblstruck 2140 Sm Gturnedsans 2141 Sm Lturnedsans 2142 Sm Lreversedsans 2143 Sm Yturnedsans 2144 Sm Ddblstruckitalic 2145 Lu ddblstruckitalic 2146 Ll edblstruckitalic 2147 Ll idblstruckitalic 2148 Ll jdblstruckitalic 2149 Ll propertyline 214A So ampersandturned 214B Sm per 214C So aktieselskab 214D So fturned 214E Ll forsamaritan 214F So # Number Forms oneseventh 2150 No oneninth 2151 No onetenth 2152 No onethird 2153 No twothirds 2154 No onefifth 2155 No twofifths 2156 No threefifths 2157 No fourfifths 2158 No onesixth 2159 No fivesixths 215A No oneeighth 215B No threeeighths 215C No fiveeighths 215D No seveneighths 215E No onefraction 215F No one.roman 2160 Nl two.roman 2161 Nl three.roman 2162 Nl four.roman 2163 Nl five.roman 2164 Nl six.roman 2165 Nl seven.roman 2166 Nl eight.roman 2167 Nl nine.roman 2168 Nl ten.roman 2169 Nl eleven.roman 216A Nl twelve.roman 216B Nl fifty.roman 216C Nl onehundred.roman 216D Nl fivehundred.roman 216E Nl onethousand.roman 216F Nl one.romansmall 2170 Nl two.romansmall 2171 Nl three.romansmall 2172 Nl four.romansmall 2173 Nl five.romansmall 2174 Nl six.romansmall 2175 Nl seven.romansmall 2176 Nl eight.romansmall 2177 Nl nine.romansmall 2178 Nl ten.romansmall 2179 Nl eleven.romansmall 217A Nl twelve.romansmall 217B Nl fifty.romansmall 217C Nl onehundred.romansmall 217D Nl fivehundred.romansmall 217E Nl onethousand.romansmall 217F Nl onethousandcd.roman 2180 Nl fivethousand.roman 2181 Nl tenthousand.roman 2182 Nl reversedonehundred.roman 2183 Lu creversed 2184 Ll sixlateform.roman 2185 Nl fiftyearlyform.roman 2186 Nl fiftythousand.roman 2187 Nl onehundredthousand.roman 2188 Nl zerothirds 2189 No turneddigittwo 218A So turneddigitthree 218B So # Arrows arrowleft 2190 Sm arrowup 2191 Sm arrowright 2192 Sm arrowdown 2193 Sm arrowleftright 2194 Sm arrowupdown 2195 So arrowNW 2196 So arrowNE 2197 So arrowSE 2198 So arrowSW 2199 So arrowleftstroke 219A Sm arrowrightstroke 219B Sm arrowleftwave 219C So arrowrightwave 219D So arrowlefttwoheaded 219E So arrowuptwoheaded 219F So arrowrighttwoheaded 21A0 Sm arrowdowntwoheaded 21A1 So arrowlefttail 21A2 So arrowrighttail 21A3 Sm arrowleftfrombar 21A4 So arrowupfrombar 21A5 So arrowrightfrombar 21A6 Sm arrowdownfrombar 21A7 So arrowupdownwithbase 21A8 So arrowlefthook 21A9 So arrowrighthook 21AA So arrowleftloop 21AB So arrowrightloop 21AC So arrowleftrightwave 21AD So arrowleftrightstroke 21AE Sm arrowdownzigzag 21AF So arrowleftuptip 21B0 So arrowuprighttip 21B1 So arrowleftdowntip 21B2 So arrowrightdowntip 21B3 So arrowrightdowncorner 21B4 So arrowleftdowncorner 21B5 So arrowanticlockwisesemicircle 21B6 So arrowclockwisesemicircle 21B7 So arrowlongNWtobar 21B8 So arrowleftoverrighttobar 21B9 So arrowanticlockwiseopencircle 21BA So arrowclockwiseopencircle 21BB So harpoonleftbarbup 21BC So harpoonleftbarbdown 21BD So harpoonupbarbright 21BE So harpoonupbarbleft 21BF So harpoonrightbarbup 21C0 So harpoonrightbarbdown 21C1 So harpoondownbarbright 21C2 So harpoondownbarbleft 21C3 So rightarrowoverleftarrow 21C4 So uparrowleftofdownarrow 21C5 So leftarrowoverrightarrow 21C6 So arrowspairedleft 21C7 So arrowspairedup 21C8 So arrowspairedright 21C9 So arrowspaireddown 21CA So leftharpoonoverrightharpoon 21CB So rightharpoonoverleftharpoon 21CC So dblarrowleftstroke 21CD So dblarrowleftrightstroke 21CE Sm dblarrowrightstroke 21CF Sm dblarrowleft 21D0 So dblarrowup 21D1 So dblarrowright 21D2 Sm dblarrowdown 21D3 So dblarrowleftright 21D4 Sm dblarrowupdown 21D5 So dblarrowNW 21D6 So dblarrowNE 21D7 So dblarrowSE 21D8 So dblarrowSW 21D9 So triplearrowleft 21DA So triplearrowright 21DB So arrowleftsquiggle 21DC So arrowrightsquiggle 21DD So dblstrokearrowup 21DE So dblstrokearrowdown 21DF So arrowleftdashed 21E0 So arrowupdashed 21E1 So arrowrightdashed 21E2 So arrowdowndashed 21E3 So arrowlefttobar 21E4 So arrowrighttobar 21E5 So whitearrowleft 21E6 So whitearrowup 21E7 So whitearrowright 21E8 So whitearrowdown 21E9 So whitearrowupfrombar 21EA So whitearrowonpedestalup 21EB So horizontalbarwhitearrowonpedestalup 21EC So verticalbarwhitearrowonpedestalup 21ED So whitedblarrowup 21EE So whitedblarrowonpedestalup 21EF So whitearrowfromwallright 21F0 So tocornerarrowNW 21F1 So tocornerarrowSE 21F2 So whitearrowupdown 21F3 So arrowrightsmallcircle 21F4 Sm downwarrowleftofuparrow 21F5 Sm threerightarrows 21F6 Sm verticalstrokearrowleft 21F7 Sm verticalstrokearrowright 21F8 Sm verticalstrokearrowleftright 21F9 Sm verticalsdbltrokearrowleft 21FA Sm verticalsdbltrokearrowright 21FB Sm verticalsdbltrokearrowleftright 21FC Sm openheadarrowleft 21FD Sm openheadarrowright 21FE Sm openheadarrowleftright 21FF Sm # Mathematical Operators universal 2200 Sm complement 2201 Sm partialdiff 2202 Sm existential 2203 Sm notexistential 2204 Sm emptyset 2205 Sm increment 2206 Sm gradient 2207 Sm element 2208 Sm notelement 2209 Sm elementsmall 220A Sm suchthat 220B Sm notcontains 220C Sm containsasmembersmall 220D Sm endpro 220E Sm product 220F Sm coproductarray 2210 Sm summation 2211 Sm minus 2212 Sm minusplus 2213 Sm dotplus 2214 Sm divisionslash 2215 Sm setminus 2216 Sm asteriskmath 2217 Sm ringoperator 2218 Sm bulletoperator 2219 Sm radical 221A Sm math:cuberoot 221B Sm math:fourthroot 221C Sm proportional 221D Sm infinity 221E Sm orthogonal 221F Sm angle 2220 Sm measuredangle 2221 Sm sphericalangle 2222 Sm divides 2223 Sm doesnotdivide 2224 Sm parallel 2225 Sm notparallel 2226 Sm logicaland 2227 Sm logicalor 2228 Sm intersection 2229 Sm union 222A Sm integral 222B Sm integraldbl 222C Sm integraltpl 222D Sm integralcontour 222E Sm integralsurface 222F Sm integralvolume 2230 Sm integralclockwise 2231 Sm integralcontourclockwise 2232 Sm integralcontouranticlockwise 2233 Sm therefore 2234 Sm because 2235 Sm ratio 2236 Sm proportion 2237 Sm dotminus 2238 Sm excess 2239 Sm geometricproportion 223A Sm homotic 223B Sm similar 223C Sm tildereversed 223D Sm lazysinverted 223E Sm sinewave 223F Sm wreathproduct 2240 Sm nottilde 2241 Sm minustilde 2242 Sm asympticallyequal 2243 Sm notasympticallyequal 2244 Sm congruent 2245 Sm approximatelybutnotactuallyequal 2246 Sm neirapproximatelynoractuallyequal 2247 Sm approxequal 2248 Sm notalmostequal 2249 Sm almostequalorequal 224A Sm tildetpl 224B Sm allequal 224C Sm equivalent 224D Sm geometricallyequivalent 224E Sm differencebetween 224F Sm approacheslimit 2250 Sm geometricallyequal 2251 Sm approximatelyequalorimage 2252 Sm imageorapproximatelyequal 2253 Sm colonequals 2254 Sm equalscolon 2255 Sm ringinequal 2256 Sm ringequal 2257 Sm corresponds 2258 Sm estimates 2259 Sm equiangular 225A Sm starequals 225B Sm deltaequal 225C Sm equalbydefinition 225D Sm measuredby 225E Sm questionedequal 225F Sm notequal 2260 Sm equivalence 2261 Sm notidentical 2262 Sm strictlyequivalent 2263 Sm lessequal 2264 Sm greaterequal 2265 Sm lessoverequal 2266 Sm greateroverequal 2267 Sm lessbutnotequal 2268 Sm greaterbutnotequal 2269 Sm muchless 226A Sm muchgreater 226B Sm between 226C Sm notequivalent 226D Sm notless 226E Sm notgreater 226F Sm neirlessnorequal 2270 Sm neirgreaternorequal 2271 Sm lessorequivalent 2272 Sm greaterorequivalent 2273 Sm neirlessnorequivalent 2274 Sm neirgreaternorequivalent 2275 Sm lessorgreater 2276 Sm greaterorless 2277 Sm neirlessnorgreater 2278 Sm neirgreaternorless 2279 Sm precedes 227A Sm succeeds 227B Sm precedesorequal 227C Sm succeedsorequal 227D Sm precedesorequivalent 227E Sm succeedsorequivalent 227F Sm doesnotprecede 2280 Sm doesnotsucceed 2281 Sm propersubset 2282 Sm propersuperset 2283 Sm notsubset 2284 Sm notasersetup 2285 Sm reflexsubset 2286 Sm reflexsuperset 2287 Sm neirasubsetnorequal 2288 Sm neirasersetnorequalup 2289 Sm subsetnotequal 228A Sm sersetnotequalup 228B Sm multiset 228C Sm multisetmultiplication 228D Sm multisetunion 228E Sm squareimage 228F Sm squareoriginal 2290 Sm squareimageorequal 2291 Sm squareoriginalorequal 2292 Sm squarecap 2293 Sm squarecup 2294 Sm circleplus 2295 Sm circledminus 2296 Sm circlemultiply 2297 Sm circleddivisionslash 2298 Sm circleddotoperator 2299 Sm circledringoperator 229A Sm circledasteriskoperator 229B Sm circledequals 229C Sm circleddash 229D Sm squaredplus 229E Sm squaredminus 229F Sm squaredtimes 22A0 Sm squareddotoperator 22A1 Sm tackright 22A2 Sm tackleft 22A3 Sm tackdown 22A4 Sm tackup 22A5 Sm assertion 22A6 Sm models 22A7 Sm true 22A8 Sm forces 22A9 Sm turnstiletplverticalbarright 22AA Sm turnstiledblverticalbarright 22AB Sm doesnotprove 22AC Sm nottrue 22AD Sm doesnotforce 22AE Sm negatedturnstiledblverticalbarright 22AF Sm precedesunderrelation 22B0 Sm succeedsunderrelation 22B1 Sm normalsubgroup 22B2 Sm containsasnormalsubgroup 22B3 Sm normalsubgroorequalup 22B4 Sm containsasnormalsubgroorequalup 22B5 Sm original 22B6 Sm image 22B7 Sm multimap 22B8 Sm hermitianconjugatematrix 22B9 Sm intercalate 22BA Sm xor 22BB Sm nand 22BC Sm nor 22BD Sm anglearcright 22BE Sm triangleright 22BF Sm logicalandarray 22C0 Sm logicalorarray 22C1 Sm intersectionarray 22C2 Sm unionarray 22C3 Sm diamondoperator 22C4 Sm dotmath 22C5 Sm staroperator 22C6 Sm divisiontimes 22C7 Sm math:bowtie 22C8 Sm normalfacrsemidirectproductleft 22C9 Sm normalfacrsemidirectproductright 22CA Sm semidirectproductleft 22CB Sm semidirectproductright 22CC Sm tildeequalsreversed 22CD Sm curlylogicalor 22CE Sm curlylogicaland 22CF Sm subsetdbl 22D0 Sm sersetdblup 22D1 Sm intersectiondbl 22D2 Sm uniondbl 22D3 Sm pitchfork 22D4 Sm equalandparallel 22D5 Sm lessdot 22D6 Sm greaterdot 22D7 Sm verymuchless 22D8 Sm verymuchgreater 22D9 Sm lessequalorgreater 22DA Sm greaterequalorless 22DB Sm equalorless 22DC Sm equalorgreater 22DD Sm equalorprecedes 22DE Sm equalorsucceeds 22DF Sm doesnotprecedeorequal 22E0 Sm doesnotsucceedorequal 22E1 Sm notsquareimageorequal 22E2 Sm notsquareoriginalorequal 22E3 Sm squareimageornotequal 22E4 Sm squareoriginalornotequal 22E5 Sm lessbutnotequivalent 22E6 Sm greaterbutnotequivalent 22E7 Sm precedesbutnotequivalent 22E8 Sm succeedsbutnotequivalent 22E9 Sm notnormalsubgroup 22EA Sm doesnotcontainasnormalsubgroup 22EB Sm notnormalsubgroorequalup 22EC Sm doesnotcontainasnormalsubgroorequalup 22ED Sm ellipsisvertical 22EE Sm ellipsismidhorizontal 22EF Sm ellipsisdiagonalupright 22F0 Sm ellipsisdiagonaldownright 22F1 Sm elementlonghorizontalstroke 22F2 Sm elementverticalbarhorizontalstroke 22F3 Sm elementsmallverticalbarhorizontalstroke 22F4 Sm elementdotabove 22F5 Sm elementoverbar 22F6 Sm elementoverbarsmall 22F7 Sm elementunderbar 22F8 Sm elementtwoshorizontalstroke 22F9 Sm containslonghorizontalstroke 22FA Sm containsverticalbarhorizontalstroke 22FB Sm containssmallverticalbarhorizontalstroke 22FC Sm containsoverbar 22FD Sm containsoverbarsmall 22FE Sm znotationbagmembership 22FF Sm # Miscellaneous Technical diametersign 2300 So electricarrow 2301 So house 2302 So arrowheadup 2303 So arrowheaddown 2304 So projective 2305 So perspective 2306 So wavyline 2307 So ceilingleft 2308 Ps ceilingright 2309 Pe floorleft 230A Ps floorright 230B Pe cropbottomright 230C So cropbottomleft 230D So croptopright 230E So croptopleft 230F So revlogicalnot 2310 So lozengesquare 2311 So arc 2312 So segment 2313 So sector 2314 So telephonerecorder 2315 So positionindicator 2316 So viewdatasquare 2317 So placeofinterestsign 2318 So notsignturned 2319 So watch 231A So hourglass 231B So cornertopleft 231C So cornertopright 231D So cornerbottomleft 231E So cornerbottomright 231F So integraltp 2320 Sm integralbt 2321 Sm frown 2322 So smile 2323 So arrowheadtwobarsuphorizontal 2324 So option 2325 So eraseright 2326 So clear 2327 So board 2328 So angleleft 2329 Ps angleright 232A Pe eraseleft 232B So benzenering 232C So cylindricity 232D So allaroundprofile 232E So symmetry 232F So totalrunout 2330 So dimensionorigin 2331 So conicaltaper 2332 So slope 2333 So counterbore 2334 So countersink 2335 So beamfunc 2336 So squishquadfunc 2337 So quadequalfunc 2338 So quaddividefunc 2339 So quaddiamondfunc 233A So quadjotfunc 233B So quadcirclefunc 233C So circlestilefunc 233D So circlejotfunc 233E So slashbarfunc 233F So backslashbarfunc 2340 So quadslashfunc 2341 So quadbackslashfunc 2342 So quadlessfunc 2343 So quadgreaterfunc 2344 So vaneleftfunc 2345 So vanerightfunc 2346 So quadarrowleftfunc 2347 So quadarrowrightfunc 2348 So circlebackslashfunc 2349 So tackunderlinedownfunc 234A So deltastilefunc 234B So quadcaretdownfunc 234C So quaddeltafunc 234D So tackjotdownfunc 234E So vaneupfunc 234F So quadarrowupfunc 2350 So tackoverbarupfunc 2351 So delstilefunc 2352 So quadcaretupfunc 2353 So quaddelfunc 2354 So tackjotupfunc 2355 So vanedownfunc 2356 So quadarrowdownfunc 2357 So quoteunderlinefunc 2358 So deltaunderlinefunc 2359 So diamondunderlinefunc 235A So jotunderlinefunc 235B So circleunderlinefunc 235C So shoejotupfunc 235D So quotequadfunc 235E So circlestarfunc 235F So quadcolonfunc 2360 So tackdiaeresisupfunc 2361 So deldiaeresisfunc 2362 So stardiaeresisfunc 2363 So jotdiaeresisfunc 2364 So circlediaeresisfunc 2365 So shoestiledownfunc 2366 So shoestileleftfunc 2367 So tildediaeresisfunc 2368 So diaeresisgreaterfunc 2369 So commabarfunc 236A So deltildefunc 236B So zildefunc 236C So stiletildefunc 236D So semicolonunderlinefunc 236E So quadnotequalfunc 236F So quadquestionfunc 2370 So carettildedownfunc 2371 So carettildeupfunc 2372 So iotafunc 2373 So rhofunc 2374 So omegafunc 2375 So alphaunderlinefunc 2376 So epsilonunderlinefunc 2377 So iotaunderlinefunc 2378 So omegaunderlinefunc 2379 So alphafunc 237A So notcheckmark 237B So anglezigzagarrowdownright 237C Sm shoulderedopenbox 237D So misc:bell 237E So linemiddledotvertical 237F So insertion 2380 So continuousunderline 2381 So discontinuousunderline 2382 So emphasis 2383 So composition 2384 So centrelineverticalsquarewhite 2385 So enter 2386 So alternative 2387 So helm 2388 So circledbarnotchhorizontal 2389 So circledtriangledown 238A So brokencirclenorthwestarrow 238B So undo 238C So monostable 238D So hysteresis 238E So htypeopencircuit 238F So ltypeopencircuit 2390 So passivedown 2391 So outputpassiveup 2392 So directcurrentformtwo 2393 So softwarefunction 2394 So quadfunc 2395 So misc:decimalseparator 2396 So previouspage 2397 So nextpage 2398 So printscreen 2399 So clearscreen 239A So parenhookupleft 239B Sm parenextensionleft 239C Sm parenlowerhookleft 239D Sm parenhookupright 239E Sm parenextensionright 239F Sm parenlowerhookright 23A0 Sm bracketcornerupleftsquare 23A1 Sm bracketextensionleftsquare 23A2 Sm bracketlowercornerleftsquare 23A3 Sm bracketcorneruprightsquare 23A4 Sm bracketextensionrightsquare 23A5 Sm bracketlowercornerrightsquare 23A6 Sm brackethookupleftcurly 23A7 Sm bracketmiddlepieceleftcurly 23A8 Sm bracketlowerhookleftcurly 23A9 Sm bracketextensioncurly 23AA Sm brackethookuprightcurly 23AB Sm bracketmiddlepiecerightcurly 23AC Sm bracketlowerhookrightcurly 23AD Sm integralextension 23AE Sm lineextensionhorizontal 23AF Sm bracketsectionupleftlowerrightcurly 23B0 Sm bracketsectionuprightlowerleftcurly 23B1 Sm summationtop 23B2 Sm summationbottom 23B3 Sm brackettopsquare 23B4 So bracketbottomsquare 23B5 So bracketoverbrackettopbottomsquare 23B6 So radicalbottom 23B7 So boxlineverticalleft 23B8 So boxlineverticalright 23B9 So scanonehorizontal 23BA So scanthreehorizontal 23BB So scansevenhorizontal 23BC So scanninehorizontal 23BD So dentistrytopverticalright 23BE So dentistrybottomverticalright 23BF So dentistrycirclevertical 23C0 So dentistrycircledownhorizontal 23C1 So dentistrycircleuphorizontal 23C2 So dentistrytrianglevertical 23C3 So dentistrytriangledownhorizontal 23C4 So dentistrytriangleuphorizontal 23C5 So dentistrywavevertical 23C6 So dentistrywavedownhorizontal 23C7 So dentistrywaveuphorizontal 23C8 So dentistrydownhorizontal 23C9 So dentistryuphorizontal 23CA So dentistrytopverticalleft 23CB So dentistrybottomverticalleft 23CC So footsquare 23CD So return 23CE So eject 23CF So lineextensionvertical 23D0 So brevemetrical 23D1 So longovershortmetrical 23D2 So shortoverlongmetrical 23D3 So longovertwoshortsmetrical 23D4 So twoshortsoverlongmetrical 23D5 So twoshortsjoinedmetrical 23D6 So trisememetrical 23D7 So tetrasememetrical 23D8 So pentasememetrical 23D9 So earthground 23DA So fuse 23DB So parentop 23DC Sm parenbottom 23DD Sm brackettopcurly 23DE Sm bracketbottomcurly 23DF Sm bracketshelltop 23E0 Sm bracketshellbottom 23E1 Sm trapeziumwhite 23E2 So benzeneringcircle 23E3 So straightness 23E4 So flatness 23E5 So accurrent 23E6 So electricalintersection 23E7 So decimalexponent 23E8 So blackpointingdoubletriangleright 23E9 So blackpointingdoubletriangleleft 23EA So blackpointingdoubletriangleup 23EB So blackpointingdoubletriangledown 23EC So blackpointingdoubletrianglebarverticalright 23ED So blackpointingdoubletrianglebarverticalleft 23EE So blackpointingtriangledoublebarverticalright 23EF So alarmclock 23F0 So swatchtop 23F1 So timerclock 23F2 So hourglassflowings 23F3 So blackmediumpointingtriangleleft 23F4 So blackmediumpointingtriangleright 23F5 So blackmediumpointingtriangleup 23F6 So blackmediumpointingtriangledown 23F7 So doublebarvertical 23F8 So blackforstopsquare 23F9 So blackcircleforrecord 23FA So power 23FB So poweronoff 23FC So poweron 23FD So powersleep 23FE So observereye 23FF So # Control Pictures cntr:null 2400 So cntr:startofheading 2401 So cntr:startoftext 2402 So cntr:endoftext 2403 So cntr:endoftransmission 2404 So cntr:enquiry 2405 So cntr:acknowledge 2406 So cntr:bell 2407 So cntr:backspace 2408 So cntr:horizontaltab 2409 So cntr:linefeed 240A So cntr:verticaltab 240B So cntr:formfeed 240C So cntr:carriagereturn 240D So cntr:shiftout 240E So cntr:shiftin 240F So cntr:datalinkescape 2410 So cntr:devicecontrolone 2411 So cntr:devicecontroltwo 2412 So cntr:devicecontrolthree 2413 So cntr:devicecontrolfour 2414 So cntr:negativeacknowledge 2415 So cntr:synchronousidle 2416 So cntr:endoftransmissionblock 2417 So cntr:cancel 2418 So cntr:endofmedium 2419 So cntr:substitute 241A So cntr:escape 241B So cntr:fileseparator 241C So cntr:groupseparator 241D So cntr:recordseparator 241E So cntr:unitseparator 241F So cntr:space 2420 So cntr:delete 2421 So cntr:blank 2422 So cntr:openbox 2423 So cntr:newline 2424 So cntr:deleteformtwo 2425 So cntr:substituteformtwo 2426 So # Optical Character Recognition hook 2440 So ocr:chair 2441 So fork 2442 So invertedfork 2443 So beltbuckle 2444 So ocr:bowtie 2445 So branchbankidentification 2446 So amountofcheck 2447 So ocr:dash 2448 So customeraccountnumber 2449 So backslashdbl 244A So # Enclosed Alphanumerics onecircle 2460 No twocircle 2461 No threecircle 2462 No fourcircle 2463 No fivecircle 2464 No sixcircle 2465 No sevencircle 2466 No eightcircle 2467 No ninecircle 2468 No tencircle 2469 No elevencircle 246A No twelvecircle 246B No thirteencircle 246C No fourteencircle 246D No fifteencircle 246E No sixteencircle 246F No seventeencircle 2470 No eighteencircle 2471 No nineteencircle 2472 No twentycircle 2473 No oneparenthesized 2474 No twoparenthesized 2475 No threeparenthesized 2476 No fourparenthesized 2477 No fiveparenthesized 2478 No sixparenthesized 2479 No sevenparenthesized 247A No eightparenthesized 247B No nineparenthesized 247C No tenparenthesized 247D No elevenparenthesized 247E No twelveparenthesized 247F No thirteenparenthesized 2480 No fourteenparenthesized 2481 No fifteenparenthesized 2482 No sixteenparenthesized 2483 No seventeenparenthesized 2484 No eighteenparenthesized 2485 No nineteenparenthesized 2486 No twentyparenthesized 2487 No oneperiod 2488 No twoperiod 2489 No threeperiod 248A No fourperiod 248B No fiveperiod 248C No sixperiod 248D No sevenperiod 248E No eightperiod 248F No nineperiod 2490 No tenperiod 2491 No elevenperiod 2492 No twelveperiod 2493 No thirteenperiod 2494 No fourteenperiod 2495 No fifteenperiod 2496 No sixteenperiod 2497 No seventeenperiod 2498 No eighteenperiod 2499 No nineteenperiod 249A No twentyperiod 249B No aparenthesized 249C So bparenthesized 249D So cparenthesized 249E So dparenthesized 249F So eparenthesized 24A0 So fparenthesized 24A1 So gparenthesized 24A2 So hparenthesized 24A3 So iparenthesized 24A4 So jparenthesized 24A5 So kparenthesized 24A6 So lparenthesized 24A7 So mparenthesized 24A8 So nparenthesized 24A9 So oparenthesized 24AA So pparenthesized 24AB So qparenthesized 24AC So rparenthesized 24AD So sparenthesized 24AE So tparenthesized 24AF So uparenthesized 24B0 So vparenthesized 24B1 So wparenthesized 24B2 So xparenthesized 24B3 So yparenthesized 24B4 So zparenthesized 24B5 So Acircle 24B6 So Bcircle 24B7 So Ccircle 24B8 So Dcircle 24B9 So Ecircle 24BA So Fcircle 24BB So Gcircle 24BC So Hcircle 24BD So Icircle 24BE So Jcircle 24BF So Kcircle 24C0 So Lcircle 24C1 So Mcircle 24C2 So Ncircle 24C3 So Ocircle 24C4 So Pcircle 24C5 So Qcircle 24C6 So Rcircle 24C7 So Scircle 24C8 So Tcircle 24C9 So Ucircle 24CA So Vcircle 24CB So Wcircle 24CC So Xcircle 24CD So Ycircle 24CE So Zcircle 24CF So acircle 24D0 So bcircle 24D1 So ccircle 24D2 So dcircle 24D3 So ecircle 24D4 So fcircle 24D5 So gcircle 24D6 So hcircle 24D7 So icircle 24D8 So jcircle 24D9 So kcircle 24DA So lcircle 24DB So mcircle 24DC So ncircle 24DD So ocircle 24DE So pcircle 24DF So qcircle 24E0 So rcircle 24E1 So scircle 24E2 So tcircle 24E3 So ucircle 24E4 So vcircle 24E5 So wcircle 24E6 So xcircle 24E7 So ycircle 24E8 So zcircle 24E9 So zerocircle 24EA No elevencircleblack 24EB No twelvecircleblack 24EC No thirteencircleblack 24ED No fourteencircleblack 24EE No fifteencircleblack 24EF No sixteencircleblack 24F0 No seventeencircleblack 24F1 No eighteencircleblack 24F2 No nineteencircleblack 24F3 No twentycircleblack 24F4 No onecircledbl 24F5 No twocircledbl 24F6 No threecircledbl 24F7 No fourcircledbl 24F8 No fivecircledbl 24F9 No sixcircledbl 24FA No sevencircledbl 24FB No eightcircledbl 24FC No ninecircledbl 24FD No tencircledbl 24FE No zerocircleblack 24FF No # Box Drawing lighthorz 2500 So heavyhorz 2501 So lightvert 2502 So heavyvert 2503 So lighttrpldashhorz 2504 So heavytrpldashhorz 2505 So lighttrpldashvert 2506 So heavytrpldashvert 2507 So lightquaddashhorz 2508 So heavyquaddashhorz 2509 So lightquaddashvert 250A So heavyquaddashvert 250B So lightdnright 250C So dnlightrightheavy 250D So dnheavyrightlight 250E So heavydnright 250F So lightdnleft 2510 So dnlightleftheavy 2511 So dnheavyleftlight 2512 So heavydnleft 2513 So lightupright 2514 So uplightrightheavy 2515 So upheavyrightlight 2516 So heavyupright 2517 So lightupleft 2518 So uplightleftheavy 2519 So upheavyleftlight 251A So heavyupleft 251B So lightvertright 251C So vertlightrightheavy 251D So upheavyrightdnlight 251E So dnheavyrightuplight 251F So vertheavyrightlight 2520 So dnlightrightupheavy 2521 So uplightrightdnheavy 2522 So heavyvertright 2523 So lightvertleft 2524 So vertlightleftheavy 2525 So upheavyleftdnlight 2526 So dnheavyleftuplight 2527 So vertheavyleftlight 2528 So dnlightleftupheavy 2529 So uplightleftdnheavy 252A So heavyvertleft 252B So lightdnhorz 252C So leftheavyrightdnlight 252D So rightheavyleftdnlight 252E So dnlighthorzheavy 252F So dnheavyhorzlight 2530 So rightlightleftdnheavy 2531 So leftlightrightdnheavy 2532 So heavydnhorz 2533 So lightuphorz 2534 So leftheavyrightuplight 2535 So rightheavyleftuplight 2536 So uplighthorzheavy 2537 So upheavyhorzlight 2538 So rightlightleftupheavy 2539 So leftlightrightupheavy 253A So heavyuphorz 253B So lightverthorz 253C So leftheavyrightvertlight 253D So rightheavyleftvertlight 253E So vertlighthorzheavy 253F So upheavydnhorzlight 2540 So dnheavyuphorzlight 2541 So vertheavyhorzlight 2542 So leftupheavyrightdnlight 2543 So rightupheavyleftdnlight 2544 So leftdnheavyrightuplight 2545 So rightdnheavyleftuplight 2546 So dnlightuphorzheavy 2547 So uplightdnhorzheavy 2548 So rightlightleftvertheavy 2549 So leftlightrightvertheavy 254A So heavyverthorz 254B So lightdbldashhorz 254C So heavydbldashhorz 254D So lightdbldashvert 254E So heavydbldashvert 254F So dblhorz 2550 So dblvert 2551 So dnsngrightdbl 2552 So dndblrightsng 2553 So dbldnright 2554 So dnsngleftdbl 2555 So dndblleftsng 2556 So dbldnleft 2557 So upsngrightdbl 2558 So updblrightsng 2559 So dblupright 255A So upsngleftdbl 255B So updblleftsng 255C So dblupleft 255D So vertsngrightdbl 255E So vertdblrightsng 255F So dblvertright 2560 So vertsngleftdbl 2561 So vertdblleftsng 2562 So dblvertleft 2563 So dnsnghorzdbl 2564 So dndblhorzsng 2565 So dbldnhorz 2566 So upsnghorzdbl 2567 So updblhorzsng 2568 So dbluphorz 2569 So vertsnghorzdbl 256A So vertdblhorzsng 256B So dblverthorz 256C So lightarcdnright 256D So lightarcdnleft 256E So lightarcupleft 256F So lightarcupright 2570 So lightdiaguprightdnleft 2571 So lightdiagupleftdnright 2572 So lightdiagcross 2573 So lightleft 2574 So lightup 2575 So lightright 2576 So lightdn 2577 So heavyleft 2578 So heavyup 2579 So heavyright 257A So heavydn 257B So lightleftheavyright 257C So lightupheavydn 257D So heavyleftlightright 257E So heavyuplightdn 257F So # Block Elements upperHalfBlock 2580 So lowerOneEighthBlock 2581 So lowerOneQuarterBlock 2582 So lowerThreeEighthsBlock 2583 So lowerHalfBlock 2584 So lowerFiveEighthsBlock 2585 So lowerThreeQuartersBlock 2586 So lowerSevenEighthsBlock 2587 So fullBlock 2588 So leftSevenEighthsBlock 2589 So leftThreeQuartersBlock 258A So leftFiveEighthsBlock 258B So leftHalfBlock 258C So leftThreeEighthsBlock 258D So leftOneQuarterBlock 258E So leftOneEighthBlock 258F So rightHalfBlock 2590 So lightShade 2591 So mediumShade 2592 So darkShade 2593 So upperOneEighthBlock 2594 So rightOneEighthBlock 2595 So quadrantLowerLeft 2596 So quadrantLowerRight 2597 So quadrantUpperLeft 2598 So quadrantUpperLeftAndLowerLeftAndLowerRight 2599 So quadrantUpperLeftAndLowerRight 259A So quadrantUpperLeftAndUpperRightAndLowerLeft 259B So quadrantUpperLeftAndUpperRightAndLowerRight 259C So quadrantUpperRight 259D So quadrantUpperRightAndLowerLeft 259E So quadrantUpperRightAndLowerLeftAndLowerRight 259F So # Geometric Shapes squareblack 25A0 So squarewhite 25A1 So squarewhiteround 25A2 So squarewhitewithsquaresmallblack 25A3 So squarehorizontalfill 25A4 So squareverticalfill 25A5 So squareorthogonalcrosshatchfill 25A6 So squareupperlefttolowerrightfill 25A7 So squareupperrighttolowerleftfill 25A8 So squarediagonalcrosshatchfill 25A9 So squaresmallblack 25AA So squaresmallwhite 25AB So rectangleblack 25AC So rectanglewhite 25AD So rectangleverticalblack 25AE So rectangleverticalwhite 25AF So parallelogramblack 25B0 So parallelogramwhite 25B1 So triangleupblack 25B2 So triangleupwhite 25B3 So triangleupsmallblack 25B4 So triangleupsmallwhite 25B5 So trianglerightblack 25B6 So trianglerightwhite 25B7 Sm trianglerightsmallblack 25B8 So trianglerightsmallwhite 25B9 So pointerrightblack 25BA So pointerrightwhite 25BB So triangledownblack 25BC So triangledownwhite 25BD So triangledownsmallblack 25BE So triangledownsmallwhite 25BF So triangleleftblack 25C0 So triangleleftwhite 25C1 Sm triangleleftsmallblack 25C2 So triangleleftsmallwhite 25C3 So pointerleftblack 25C4 So pointerleftwhite 25C5 So gmtr:diamondblack 25C6 So gmtr:diamondwhite 25C7 So diamondwhitewithdiamondsmallblack 25C8 So fisheye 25C9 So lozenge 25CA So circlewhite 25CB So circledotted 25CC So circleverticalfill 25CD So bullseye 25CE So circleblack 25CF So circlehalfleftblack 25D0 So circlehalfrightblack 25D1 So circlelowerhalfblack 25D2 So circleupperhalfblack 25D3 So circleupperquadrantrightblack 25D4 So circleallbutupperquadrantleftblack 25D5 So halfcircleleftblack 25D6 So halfcirclerightblack 25D7 So bulletinverse 25D8 So circleinversewhite 25D9 So upperhalfcircleinversewhite 25DA So lowerhalfcircleinversewhite 25DB So upperquadrantcirculararcleft 25DC So upperquadrantcirculararcright 25DD So lowerquadrantcirculararcright 25DE So lowerquadrantcirculararcleft 25DF So upperhalfcircle 25E0 So lowerhalfcircle 25E1 So lowertrianglerightblack 25E2 So lowertriangleleftblack 25E3 So uppertriangleleftblack 25E4 So uppertrianglerightblack 25E5 So openbullet 25E6 So squarehalfleftblack 25E7 So squarehalfrightblack 25E8 So squareupperdiagonalhalfleftblack 25E9 So squarelowerdiagonalhalfrightblack 25EA So squarewhitebisectinglinevertical 25EB So triangledotupwhite 25EC So trianglehalfupleftblack 25ED So trianglehalfuprightblack 25EE So largecircle 25EF So squarewhiteupperquadrantleft 25F0 So squarewhitelowerquadrantleft 25F1 So squarewhitelowerquadrantright 25F2 So squarewhiteupperquadrantright 25F3 So circleupperquadrantleftwhite 25F4 So circlelowerquadrantleftwhite 25F5 So circlelowerquadrantrightwhite 25F6 So circleupperquadrantrightwhite 25F7 So uppertriangleleft 25F8 Sm uppertriangleright 25F9 Sm lowertriangleleft 25FA Sm squaremediumwhite 25FB Sm squaremediumblack 25FC Sm squaresmallmediumwhite 25FD Sm squaresmallmediumblack 25FE Sm lowertriangleright 25FF Sm # Miscellaneous Symbols sunraysblack 2600 So cloud 2601 So umbrella 2602 So snowman 2603 So comet 2604 So starblack 2605 So starwhite 2606 So lightning 2607 So thunderstorm 2608 So sun 2609 So nodeascending 260A So nodedescending 260B So conjunction 260C So opposition 260D So telephoneblack 260E So telephonewhite 260F So checkbox 2610 So checkboxchecked 2611 So checkboxx 2612 So saltire 2613 So umbrellaraindrops 2614 So hotbeverage 2615 So shogipiecewhite 2616 So shogipieceblack 2617 So shamrock 2618 So floralheartbulletreversedrotated 2619 So pointingindexleftblack 261A So pointingindexrightblack 261B So pointingindexleftwhite 261C So pointingindexupwhite 261D So pointingindexrightwhite 261E So pointingindexdownwhite 261F So skullcrossbones 2620 So caution 2621 So radioactive 2622 So biohazard 2623 So caduceus 2624 So ankh 2625 So orthodoxcross 2626 So chirho 2627 So crossoflorraine 2628 So crossofjerusalem 2629 So starcrescent 262A So farsi 262B So adishakti 262C So hammersickle 262D So peace 262E So yinyang 262F So trigramheaven 2630 So trigramlake 2631 So trigramfire 2632 So trigramthunder 2633 So trigramwind 2634 So trigramwater 2635 So trigrammountain 2636 So trigramearth 2637 So wheelofdharma 2638 So frowningfacewhite 2639 So smilingfacewhite 263A So smilingfaceblack 263B So sunrayswhite 263C So firstquartermoon 263D So lastquartermoon 263E So mercury 263F So female 2640 So earth 2641 So male 2642 So jiterup 2643 So saturn 2644 So uranus 2645 So neptune 2646 So pluto 2647 So aries 2648 So taurus 2649 So gemini 264A So cancer 264B So leo 264C So virgo 264D So libra 264E So scorpius 264F So sagittarius 2650 So capricorn 2651 So aquarius 2652 So pisces 2653 So kingwhite 2654 So queenwhite 2655 So rookwhite 2656 So bishopwhite 2657 So knightwhite 2658 So pawnwhite 2659 So kingblack 265A So queenblack 265B So rookblack 265C So bishopblack 265D So knightblack 265E So pawnblack 265F So spadeblack 2660 So heartwhite 2661 So misc:diamondwhite 2662 So clubblack 2663 So spadewhite 2664 So heartblack 2665 So misc:diamondblack 2666 So clubwhite 2667 So hotsprings 2668 So quarternote 2669 So eighthnote 266A So beamedeighthnotes 266B So beamedsixteenthnotes 266C So musicflat 266D So musicnatural 266E So musicsharp 266F Sm westsyriaccross 2670 So eastsyriaccross 2671 So recycleuniversal 2672 So recycleoneplastics 2673 So recycletwoplastics 2674 So recyclethreeplastics 2675 So recyclefourplastics 2676 So recyclefiveplastics 2677 So recyclesixplastics 2678 So recyclesevenplastics 2679 So recyclegeneric 267A So recycleuniversalblack 267B So recycledpaper 267C So recyclepartiallypaper 267D So permanentpaper 267E So wheelchair 267F So dieone 2680 So dietwo 2681 So diethree 2682 So diefour 2683 So diefive 2684 So diesix 2685 So circledotrightwhite 2686 So circletwodotswhite 2687 So circlewhitedotrightblack 2688 So circletwodotsblackwhite 2689 So monogramyang 268A So monogramyin 268B So digramgreateryang 268C So digramlesseryin 268D So digramlesseryang 268E So digramgreateryin 268F So flagwhite 2690 So flagblack 2691 So hammerpick 2692 So anchor 2693 So crossedswords 2694 So staffofaesculapius 2695 So scales 2696 So alembic 2697 So flower 2698 So gear 2699 So staffofhermes 269A So atom 269B So fleurdelis 269C So staroutlinedwhite 269D So threelinesconvergingright 269E So threelinesconvergingleft 269F So warning 26A0 So highvoltage 26A1 So dfemaledbl 26A2 So dmaledbl 26A3 So interlockedfemalemale 26A4 So malefemale 26A5 So malestroke 26A6 So malestrokemalefemale 26A7 So verticalmalestroke 26A8 So horizontalmalestroke 26A9 So mediumcirclewhite 26AA So mediumcircleblack 26AB So mediumsmallcirclewhite 26AC So marriage 26AD So divorce 26AE So unmarriedpartnership 26AF So coffin 26B0 So funeralurn 26B1 So neuter 26B2 So ceres 26B3 So pallas 26B4 So juno 26B5 So vesta 26B6 So chiron 26B7 So moonlilithblack 26B8 So sextile 26B9 So semisextile 26BA So quincunx 26BB So sesquiquadrate 26BC So soccerball 26BD So baseball 26BE So squaredkey 26BF So draughtsmanwhite 26C0 So draughtskingwhite 26C1 So draughtsmanblack 26C2 So draughtskingblack 26C3 So snowmanoutsnow 26C4 So sunbehindcloud 26C5 So rain 26C6 So snowmanblack 26C7 So thundercloudrain 26C8 So turnedshogipiecewhite 26C9 So turnedshogipieceblack 26CA So diamondinsquarewhite 26CB So crossinglanes 26CC So disabledcar 26CD So ophiuchus 26CE So pick 26CF So carsliding 26D0 So helmetcrosswhite 26D1 So circledcrossinglanes 26D2 So chains 26D3 So noentry 26D4 So alternateonewayleftwaytraffic 26D5 So twowayleftwaytrafficblack 26D6 So twowayleftwaytrafficwhite 26D7 So lanemergeleftblack 26D8 So lanemergeleftwhite 26D9 So driveslow 26DA So pointingtriangledownheavywhite 26DB So closedentryleft 26DC So squaredsaltire 26DD So fallingdiagonalincircleinsquareblackwhite 26DE So truckblack 26DF So restrictedentryoneleft 26E0 So restrictedentrytwoleft 26E1 So astronomicaluranus 26E2 So circlestroketwodotsaboveheavy 26E3 So pentagram 26E4 So hedinterlacedpentagramright 26E5 So hedinterlacedpentagramleft 26E6 So invertedpentagram 26E7 So crossonshieldblack 26E8 So shintoshrine 26E9 So church 26EA So castle 26EB So historicsite 26EC So gearouthub 26ED So gearhles 26EE So maplighthouse 26EF So mountain 26F0 So umbrellaonground 26F1 So fountain 26F2 So flaginhole 26F3 So ferry 26F4 So sailboat 26F5 So squarefourcorners 26F6 So skier 26F7 So iceskate 26F8 So personball 26F9 So tent 26FA So japanesebank 26FB So headstonegraveyard 26FC So fuelpump 26FD So consquareupblack 26FE So flaghorizontalmiddlestripeblackwhite 26FF So # Dingbats safetyscissorsblack 2700 So scissorsupperblade 2701 So scissorsblack 2702 So scissorslowerblade 2703 So scissorswhite 2704 So checkwhiteheavy 2705 So telephonelocationsign 2706 So tapedrive 2707 So airplane 2708 So envelope 2709 So raisedfist 270A So raisedh 270B So hvictory 270C So hwriting 270D So pencillowerright 270E So pencil 270F So pencilupperright 2710 So nibwhite 2711 So nibblack 2712 So check 2713 So checkheavy 2714 So multiplicationx 2715 So multiplicationxheavy 2716 So ballotx 2717 So ballotxheavy 2718 So greekcrossoutlined 2719 So greekcrossheavy 271A So crosscentreopen 271B So crosscentreopenheavy 271C So latincross 271D So latincrossshadowedwhite 271E So latincrossoutlined 271F So maltesecross 2720 So starofdavid 2721 So asteriskteardropfour 2722 So asteriskballoonfour 2723 So asteriskballoonheavyfour 2724 So asteriskclubfour 2725 So starpointedblackfour 2726 So starpointedwhitefour 2727 So sparkles 2728 So staroutlinedstresswhite 2729 So starcircledwhite 272A So starcentreopenblack 272B So starcentreblackwhite 272C So staroutlinedblack 272D So staroutlinedblackheavy 272E So starpinwheel 272F So starshadowedwhite 2730 So asteriskheavy 2731 So asteriskcentreopen 2732 So spokedasteriskeight 2733 So starpointedblackeight 2734 So starpointedpinwheeleight 2735 So starpointedblacksix 2736 So compasstarpointedblackeight 2737 So compasstarpointedblackheavyeight 2738 So starpointedblacktwelve 2739 So asteriskpointedsixteen 273A So asteriskteardrop 273B So asteriskteardropcentreopen 273C So asteriskteardropheavy 273D So florettepetalledblackwhitesix 273E So floretteblack 273F So florettewhite 2740 So floretteoutlinedpetalledblackeight 2741 So starcentreopenpointedcircledeight 2742 So asteriskteardroppinwheelheavy 2743 So snowflake 2744 So snowflaketight 2745 So chevronsnowflakeheavy 2746 So sparkle 2747 So sparkleheavy 2748 So asteriskballoon 2749 So asteriskteardroppropellereight 274A So asteriskteardroppropellerheavyeight 274B So cross 274C So circleshadowedwhite 274D So squaredcrossnegative 274E So squareshadowlowerrightwhite 274F So squareshadowupperrightwhite 2750 So squarelowerrightshadowedwhite 2751 So squareupperrightshadowedwhite 2752 So questionblackornament 2753 So questionwhiteornament 2754 So exclamationwhiteornament 2755 So diamondminusxblackwhite 2756 So exclamationheavy 2757 So verticalbarlight 2758 So verticalbarmedium 2759 So verticalbarheavy 275A So commaheavyturnedornament 275B So commaheavyornament 275C So commaheavydoubleturnedornament 275D So commaheavydoubleornament 275E So lowcommaheavyornament 275F So lowcommaheavydoubleornament 2760 So curvedstemparagraphsignornament 2761 So exclamationheavyornament 2762 So heartexclamationheavyornament 2763 So heartblackheavy 2764 So heartbulletrotatedblackheavy 2765 So floralheart 2766 So floralheartbulletrotated 2767 So parenthesisleftmediumornament 2768 Ps parenthesisrightmediumornament 2769 Pe parenthesisleftflattenedmediumornament 276A Ps parenthesisrightflattenedmediumornament 276B Pe bracketleftpointedanglemediumornament 276C Ps bracketrightpointedanglemediumornament 276D Pe quotationleftpointedangleheavyornament 276E Ps quotationrightpointedangleheavyornament 276F Pe bracketleftpointedangleheavyornament 2770 Ps bracketrightpointedangleheavyornament 2771 Pe bracketshellleftlightornament 2772 Ps bracketshellrightlightornament 2773 Pe curlybracketleftmediumornament 2774 Ps curlybracketrightmediumornament 2775 Pe onenegativecircled 2776 No twonegativecircled 2777 No threenegativecircled 2778 No fournegativecircled 2779 No fivenegativecircled 277A No sixnegativecircled 277B No sevennegativecircled 277C No eightnegativecircled 277D No ninenegativecircled 277E No tennegativecircled 277F No onesanscircled 2780 No twosanscircled 2781 No threesanscircled 2782 No foursanscircled 2783 No fivesanscircled 2784 No sixsanscircled 2785 No sevensanscircled 2786 No eightsanscircled 2787 No ninesanscircled 2788 No tensanscircled 2789 No onesansnegativecircled 278A No twosansnegativecircled 278B No threesansnegativecircled 278C No foursansnegativecircled 278D No fivesansnegativecircled 278E No sixsansnegativecircled 278F No sevensansnegativecircled 2790 No eightsansnegativecircled 2791 No ninesansnegativecircled 2792 No tensansnegativecircled 2793 No arrowrightwideheavy 2794 So plussignheavy 2795 So minussignheavy 2796 So divisionsignheavy 2797 So arrowheavySE 2798 So arrowrightheavy 2799 So arrowheavyNE 279A So arrowrightpointed 279B So arrowrightroundheavy 279C So arrowrighttriangle 279D So arrowrighttriangleheavy 279E So arrowrighttriangledashed 279F So arrowrighttriangledashedheavy 27A0 So arrowrightblack 27A1 So arrowheadrightthreeDtoplight 27A2 So arrowheadrightthreeDbottomlight 27A3 So arrowheadrightblack 27A4 So arrowrightcurvedownblackheavy 27A5 So arrowrightcurveupblackheavy 27A6 So arrowrightsquatblack 27A7 So arrowrightpointedblackheavy 27A8 So arrowrightrightshadedwhite 27A9 So arrowrightleftshadedwhite 27AA So arrowrightbacktiltedshadowedwhite 27AB So arrowrightfronttiltedshadowedwhite 27AC So arrowshadowrightlowerwhiteheavy 27AD So arrowshadowrightupperwhiteheavy 27AE So arrowshadowrightnotchedlowerwhite 27AF So curlyloop 27B0 So arrowshadowrightnotchedupperwhite 27B1 So arrowrightcircledwhiteheavy 27B2 So arrowrightfeatheredwhite 27B3 So arrowfeatheredblackSE 27B4 So arrowrightfeatheredblack 27B5 So arrowfeatheredblackNE 27B6 So arrowfeatheredblackheavySE 27B7 So arrowrightfeatheredblackheavy 27B8 So arrowfeatheredblackheavyNE 27B9 So arrowteardropright 27BA So arrowteardroprightheavy 27BB So arrowrightwedge 27BC So arrowrightwedgeheavy 27BD So arrowrightoutlinedopen 27BE So curlyloopdouble 27BF So # Miscellaneous Mathematical Symbols-A threedimensionalangle 27C0 Sm tricontainingtriwhiteanglesmall 27C1 Sm perpendicular 27C2 Sm opensubset 27C3 Sm opensuperset 27C4 Sm bagdelimitersshapeleft 27C5 Ps bagdelimitersshaperight 27C6 Pe ordotinside 27C7 Sm solidussubsetreversepreceding 27C8 Sm solidussupersetpreceding 27C9 Sm verticalbarhorizontalstroke 27CA Sm risingdiagonal 27CB Sm longdivision 27CC Sm fallingdiagonal 27CD Sm dlogicalsquare 27CE Sm dlogicalorsquare 27CF Sm centreddotwhitediamond 27D0 Sm dot 27D1 Sm elementopeningup 27D2 Sm lowercornerdotright 27D3 Sm cornerdotupleft 27D4 Sm outerjoinleft 27D5 Sm outerjoinright 27D6 Sm outerjoinfull 27D7 Sm largetackup 27D8 Sm largetackdown 27D9 Sm turnstileleftrightdbl 27DA Sm tackleftright 27DB Sm multimapleft 27DC Sm longtackright 27DD Sm longtackleft 27DE Sm tackcircleaboveup 27DF Sm lozengedividedbyrulehorizontal 27E0 Sm convavediamondwhite 27E1 Sm tickconvavediamondleftwhite 27E2 Sm tickconvavediamondrightwhite 27E3 Sm tickleftwhitesquare 27E4 Sm tickrightwhitesquare 27E5 Sm bracketwhitesquareleft 27E6 Ps bracketwhitesquareright 27E7 Pe bracketangleleft 27E8 Ps bracketangleright 27E9 Pe bracketangledblleft 27EA Ps bracketangledblright 27EB Pe bracketshellwhiteleft 27EC Ps bracketshellwhiteright 27ED Pe parenflatleft 27EE Ps parenflatright 27EF Pe # Braille Patterns brblank 2800 So dots1 2801 So dots2 2802 So dots12 2803 So dots3 2804 So dots13 2805 So dots23 2806 So dots123 2807 So dots4 2808 So dots14 2809 So dots24 280A So dots124 280B So dots34 280C So dots134 280D So dots234 280E So dots1234 280F So dots5 2810 So dots15 2811 So dots25 2812 So dots125 2813 So dots35 2814 So dots135 2815 So dots235 2816 So dots1235 2817 So dots45 2818 So dots145 2819 So dots245 281A So dots1245 281B So dots345 281C So dots1345 281D So dots2345 281E So dots12345 281F So dots6 2820 So dots16 2821 So dots26 2822 So dots126 2823 So dots36 2824 So dots136 2825 So dots236 2826 So dots1236 2827 So dots46 2828 So dots146 2829 So dots246 282A So dots1246 282B So dots346 282C So dots1346 282D So dots2346 282E So dots12346 282F So dots56 2830 So dots156 2831 So dots256 2832 So dots1256 2833 So dots356 2834 So dots1356 2835 So dots2356 2836 So dots12356 2837 So dots456 2838 So dots1456 2839 So dots2456 283A So dots12456 283B So dots3456 283C So dots13456 283D So dots23456 283E So dots123456 283F So dots7 2840 So dots17 2841 So dots27 2842 So dots127 2843 So dots37 2844 So dots137 2845 So dots237 2846 So dots1237 2847 So dots47 2848 So dots147 2849 So dots247 284A So dots1247 284B So dots347 284C So dots1347 284D So dots2347 284E So dots12347 284F So dots57 2850 So dots157 2851 So dots257 2852 So dots1257 2853 So dots357 2854 So dots1357 2855 So dots2357 2856 So dots12357 2857 So dots457 2858 So dots1457 2859 So dots2457 285A So dots12457 285B So dots3457 285C So dots13457 285D So dots23457 285E So dots123457 285F So dots67 2860 So dots167 2861 So dots267 2862 So dots1267 2863 So dots367 2864 So dots1367 2865 So dots2367 2866 So dots12367 2867 So dots467 2868 So dots1467 2869 So dots2467 286A So dots12467 286B So dots3467 286C So dots13467 286D So dots23467 286E So dots123467 286F So dots567 2870 So dots1567 2871 So dots2567 2872 So dots12567 2873 So dots3567 2874 So dots13567 2875 So dots23567 2876 So dots123567 2877 So dots4567 2878 So dots14567 2879 So dots24567 287A So dots124567 287B So dots34567 287C So dots134567 287D So dots234567 287E So dots1234567 287F So dots8 2880 So dots18 2881 So dots28 2882 So dots128 2883 So dots38 2884 So dots138 2885 So dots238 2886 So dots1238 2887 So dots48 2888 So dots148 2889 So dots248 288A So dots1248 288B So dots348 288C So dots1348 288D So dots2348 288E So dots12348 288F So dots58 2890 So dots158 2891 So dots258 2892 So dots1258 2893 So dots358 2894 So dots1358 2895 So dots2358 2896 So dots12358 2897 So dots458 2898 So dots1458 2899 So dots2458 289A So dots12458 289B So dots3458 289C So dots13458 289D So dots23458 289E So dots123458 289F So dots68 28A0 So dots168 28A1 So dots268 28A2 So dots1268 28A3 So dots368 28A4 So dots1368 28A5 So dots2368 28A6 So dots12368 28A7 So dots468 28A8 So dots1468 28A9 So dots2468 28AA So dots12468 28AB So dots3468 28AC So dots13468 28AD So dots23468 28AE So dots123468 28AF So dots568 28B0 So dots1568 28B1 So dots2568 28B2 So dots12568 28B3 So dots3568 28B4 So dots13568 28B5 So dots23568 28B6 So dots123568 28B7 So dots4568 28B8 So dots14568 28B9 So dots24568 28BA So dots124568 28BB So dots34568 28BC So dots134568 28BD So dots234568 28BE So dots1234568 28BF So dots78 28C0 So dots178 28C1 So dots278 28C2 So dots1278 28C3 So dots378 28C4 So dots1378 28C5 So dots2378 28C6 So dots12378 28C7 So dots478 28C8 So dots1478 28C9 So dots2478 28CA So dots12478 28CB So dots3478 28CC So dots13478 28CD So dots23478 28CE So dots123478 28CF So dots578 28D0 So dots1578 28D1 So dots2578 28D2 So dots12578 28D3 So dots3578 28D4 So dots13578 28D5 So dots23578 28D6 So dots123578 28D7 So dots4578 28D8 So dots14578 28D9 So dots24578 28DA So dots124578 28DB So dots34578 28DC So dots134578 28DD So dots234578 28DE So dots1234578 28DF So dots678 28E0 So dots1678 28E1 So dots2678 28E2 So dots12678 28E3 So dots3678 28E4 So dots13678 28E5 So dots23678 28E6 So dots123678 28E7 So dots4678 28E8 So dots14678 28E9 So dots24678 28EA So dots124678 28EB So dots34678 28EC So dots134678 28ED So dots234678 28EE So dots1234678 28EF So dots5678 28F0 So dots15678 28F1 So dots25678 28F2 So dots125678 28F3 So dots35678 28F4 So dots135678 28F5 So dots235678 28F6 So dots1235678 28F7 So dots45678 28F8 So dots145678 28F9 So dots245678 28FA So dots1245678 28FB So dots345678 28FC So dots1345678 28FD So dots2345678 28FE So dots12345678 28FF So # Glagolitic Azu 2C00 Lu Buky 2C01 Lu Vede 2C02 Lu Glagoli 2C03 Lu Dobro 2C04 Lu Yestu 2C05 Lu Zhivete 2C06 Lu Dzelo 2C07 Lu Zemlja 2C08 Lu Izhe 2C09 Lu Initializhe 2C0A Lu glag:I 2C0B Lu Djervi 2C0C Lu Kako 2C0D Lu Ljudije 2C0E Lu Myslite 2C0F Lu Nashi 2C10 Lu Onu 2C11 Lu Pokoji 2C12 Lu Ritsi 2C13 Lu Slovo 2C14 Lu Tvrido 2C15 Lu Uku 2C16 Lu Fritu 2C17 Lu Heru 2C18 Lu Otu 2C19 Lu Pe 2C1A Lu Shta 2C1B Lu Tsi 2C1C Lu Chrivi 2C1D Lu Sha 2C1E Lu Yeru 2C1F Lu Yeri 2C20 Lu Yati 2C21 Lu Spideryha 2C22 Lu Yu 2C23 Lu Smallyus 2C24 Lu Smallyuswithtail 2C25 Lu Yo 2C26 Lu Iotatedsmallyus 2C27 Lu Bigyus 2C28 Lu Iotatedbigyus 2C29 Lu Fita 2C2A Lu Izhitsa 2C2B Lu Shtapic 2C2C Lu Trokutastia 2C2D Lu Latinatemyslite 2C2E Lu azu 2C30 Ll buky 2C31 Ll vede 2C32 Ll glagoli 2C33 Ll dobro 2C34 Ll yestu 2C35 Ll zhivete 2C36 Ll dzelo 2C37 Ll zemlja 2C38 Ll izhe 2C39 Ll initializhe 2C3A Ll glag:i 2C3B Ll djervi 2C3C Ll kako 2C3D Ll ljudije 2C3E Ll myslite 2C3F Ll nashi 2C40 Ll onu 2C41 Ll pokoji 2C42 Ll ritsi 2C43 Ll slovo 2C44 Ll tvrido 2C45 Ll uku 2C46 Ll fritu 2C47 Ll heru 2C48 Ll otu 2C49 Ll pe 2C4A Ll shta 2C4B Ll tsi 2C4C Ll chrivi 2C4D Ll sha 2C4E Ll yeru 2C4F Ll yeri 2C50 Ll yati 2C51 Ll spideryha 2C52 Ll glag:yu 2C53 Ll smallyus 2C54 Ll smallyuswithtail 2C55 Ll yo 2C56 Ll iotatedsmallyus 2C57 Ll bigyus 2C58 Ll iotatedbigyus 2C59 Ll fita 2C5A Ll izhitsa 2C5B Ll shtapic 2C5C Ll trokutastia 2C5D Ll latinatemyslite 2C5E Ll # Latin Extended-C Ldblbar 2C60 Lu ldblbar 2C61 Ll Lmiddletilde 2C62 Lu Pstroke 2C63 Lu Rtail 2C64 Lu astroke 2C65 Ll tstroke 2C66 Ll Hdescender 2C67 Lu hdescender 2C68 Ll Kdescender 2C69 Lu kdescender 2C6A Ll Zdescender 2C6B Lu zdescender 2C6C Ll lt:Alpha 2C6D Lu Mhook 2C6E Lu Aturned 2C6F Lu lt:Alphaturned 2C70 Lu vrighthook 2C71 Ll Whook 2C72 Lu whook 2C73 Ll vcurl 2C74 Ll Hhalf 2C75 Lu hhalf 2C76 Ll phitailless 2C77 Ll enotch 2C78 Ll rtailturned 2C79 Ll olowringinside 2C7A Ll Esmallturned 2C7B Ll j.inferior 2C7C Lm Vmod 2C7D Lm Sswashtail 2C7E Lu Zswashtail 2C7F Lu # Georgian Supplement anGeok 2D00 Ll banGeok 2D01 Ll ganGeok 2D02 Ll donGeok 2D03 Ll enGeok 2D04 Ll vinGeok 2D05 Ll zenGeok 2D06 Ll tanGeok 2D07 Ll inGeok 2D08 Ll kanGeok 2D09 Ll lasGeok 2D0A Ll manGeok 2D0B Ll narGeok 2D0C Ll onGeok 2D0D Ll parGeok 2D0E Ll zharGeok 2D0F Ll raeGeok 2D10 Ll sanGeok 2D11 Ll tarGeok 2D12 Ll unGeok 2D13 Ll pharGeok 2D14 Ll kharGeok 2D15 Ll ghanGeok 2D16 Ll qarGeok 2D17 Ll shinGeok 2D18 Ll chinGeok 2D19 Ll canGeok 2D1A Ll jilGeok 2D1B Ll cilGeok 2D1C Ll charGeok 2D1D Ll xanGeok 2D1E Ll jhanGeok 2D1F Ll haeGeok 2D20 Ll heGeok 2D21 Ll hieGeok 2D22 Ll weGeok 2D23 Ll harGeok 2D24 Ll hoeGeok 2D25 Ll ynGeok 2D27 Ll aenGeok 2D2D Ll # Supplemental Punctuation anglemarkersubstitutionright 2E00 Po anglemarkerdottedsubstitutionright 2E01 Po bracketsubstitutionleft 2E02 Pi bracketsubstitutionright 2E03 Pf bracketdottedsubstitutionleft 2E04 Pi bracketdottedsubstitutionright 2E05 Pf markerraisedinterpolation 2E06 Po markerdottedraisedinterpolation 2E07 Po markerdottedtransposition 2E08 Po brackettranspositionleft 2E09 Pi brackettranspositionright 2E0A Pf squareraised 2E0B Po bracketraisedleft 2E0C Pi bracketraisedright 2E0D Pf coroniseditorial 2E0E Po paragraphos 2E0F Po paragraphosforked 2E10 Po paragraphosforkedreversed 2E11 Po hypodiastole 2E12 Po obelosdotted 2E13 Po ancoradown 2E14 Po ancoraup 2E15 Po angledottedright 2E16 Po hyphendbloblique 2E17 Pd interrobanginverted 2E18 Po palmbranch 2E19 Po hyphendieresis 2E1A Pd tildering 2E1B Po bracketparaphraselowleft 2E1C Pi bracketparaphraselowright 2E1D Pf tildedotaccent 2E1E Po tildedotbelow 2E1F Po barquillverticalleft 2E20 Pi barquillverticalright 2E21 Pf brackethalftopleft 2E22 Ps brackethalftopright 2E23 Pe brackethalfbottomleft 2E24 Ps brackethalfbottomright 2E25 Pe ubracketleft 2E26 Ps ubracketright 2E27 Pe parendblleft 2E28 Ps parendblright 2E29 Pe twodotsoveronedot 2E2A Po onedotovertwodots 2E2B Po dotsquarefour 2E2C Po fivedot 2E2D Po questionreversed 2E2E Po tildevertical 2E2F Lm pointring 2E30 Po wordseparatormiddledot 2E31 Po turnedcomma 2E32 Po dotraised 2E33 Po commaraised 2E34 Po turnedsemicolon 2E35 Po daggerwithguardleft 2E36 Po daggerwithguardright 2E37 Po turneddagger 2E38 Po sectionsignhalftop 2E39 Po emdashdbl 2E3A Pd emdashtpl 2E3B Pd stenographicfullstop 2E3C Po sixdotsvertical 2E3D Po wigglylinevertical 2E3E Po capitulum 2E3F Po hyphendbl 2E40 Pd commareversed 2E41 Po quotedbllowreversed 2E42 Ps dashwithupturnleft 2E43 Po suspensiondbl 2E44 Po kavykainvertedlow 2E45 Po kavykawithkavykaaboveinvertedlow 2E46 Po kavykalow 2E47 Po kavykawithdotlow 2E48 Po stackedcommadbl 2E49 Po solidusdotted 2E4A Po tripledagger 2E4B Po medievalcomma 2E4C Po paragraphus 2E4D Po punctuselevatus 2E4E Po cornishversedivider 2E4F Po # CJK Symbols and Punctuation ideographicspace 3000 Zs ideographiccomma 3001 Po ideographicperiod 3002 Po dittomark 3003 Po jis 3004 So ideographiciterationmark 3005 Lm ideographicclose 3006 Lo ideographiczero 3007 Nl anglebracketleft 3008 Ps anglebracketright 3009 Pe dblanglebracketleft 300A Ps dblanglebracketright 300B Pe cornerbracketleft 300C Ps cornerbracketright 300D Pe whitecornerbracketleft 300E Ps whitecornerbracketright 300F Pe blacklenticularbracketleft 3010 Ps blacklenticularbracketright 3011 Pe postalmark 3012 So getamark 3013 So tortoiseshellbracketleft 3014 Ps tortoiseshellbracketright 3015 Pe whitelenticularbracketleft 3016 Ps whitelenticularbracketright 3017 Pe whitetortoiseshellbracketleft 3018 Ps whitetortoiseshellbracketright 3019 Pe whitesquarebracketleft 301A Ps whitesquarebracketright 301B Pe wavedash 301C Pd quotedblprimereversed 301D Ps quotedblprime 301E Pe lowquotedblprime 301F Pe postalmarkface 3020 So onehangzhou 3021 Nl twohangzhou 3022 Nl threehangzhou 3023 Nl fourhangzhou 3024 Nl fivehangzhou 3025 Nl sixhangzhou 3026 Nl sevenhangzhou 3027 Nl eighthangzhou 3028 Nl ninehangzhou 3029 Nl ideographicleveltonemark 302A Mn ideographicrisingtonemark 302B Mn ideographicdepartingtonemark 302C Mn ideographicenteringtonemark 302D Mn hangulsingledottonemark 302E Mc hanguldottonemarkdbl 302F Mc wavydash 3030 Pd verticalkanarepeatmark 3031 Lm verticalkanarepeatwithvoicedsoundmark 3032 Lm verticalkanarepeatmarkupperhalf 3033 Lm verticalkanarepeatwithvoicedsoundmarkupperhalf 3034 Lm verticalkanarepeatmarklowerhalf 3035 Lm circlepostalmark 3036 So ideographictelegraphlinefeedseparatorsymbol 3037 So tenhangzhou 3038 Nl twentyhangzhou 3039 Nl thirtyhangzhou 303A Nl verticalideographiciterationmark 303B Lm masumark 303C Lo partalternationmark 303D Po ideographicvariationindicator 303E So ideographichalffillspace 303F So # Hiragana hira:asmall 3041 Lo hira:a 3042 Lo hira:ismall 3043 Lo hira:i 3044 Lo hira:usmall 3045 Lo hira:u 3046 Lo hira:esmall 3047 Lo hira:e 3048 Lo hira:osmall 3049 Lo hira:o 304A Lo hira:ka 304B Lo hira:ga 304C Lo hira:ki 304D Lo hira:gi 304E Lo hira:ku 304F Lo hira:gu 3050 Lo hira:ke 3051 Lo hira:ge 3052 Lo hira:ko 3053 Lo hira:go 3054 Lo hira:sa 3055 Lo hira:za 3056 Lo hira:si 3057 Lo hira:zi 3058 Lo hira:su 3059 Lo hira:zu 305A Lo hira:se 305B Lo hira:ze 305C Lo hira:so 305D Lo hira:zo 305E Lo hira:ta 305F Lo hira:da 3060 Lo hira:ti 3061 Lo hira:di 3062 Lo hira:tusmall 3063 Lo hira:tu 3064 Lo hira:du 3065 Lo hira:te 3066 Lo hira:de 3067 Lo hira:to 3068 Lo hira:do 3069 Lo hira:na 306A Lo hira:ni 306B Lo hira:nu 306C Lo hira:ne 306D Lo hira:no 306E Lo hira:ha 306F Lo hira:ba 3070 Lo hira:pa 3071 Lo hira:hi 3072 Lo hira:bi 3073 Lo hira:pi 3074 Lo hira:hu 3075 Lo hira:bu 3076 Lo hira:pu 3077 Lo hira:he 3078 Lo hira:be 3079 Lo hira:pe 307A Lo hira:ho 307B Lo hira:bo 307C Lo hira:po 307D Lo hira:ma 307E Lo hira:mi 307F Lo hira:mu 3080 Lo hira:me 3081 Lo hira:mo 3082 Lo hira:yasmall 3083 Lo hira:ya 3084 Lo hira:yusmall 3085 Lo hira:yu 3086 Lo hira:yosmall 3087 Lo hira:yo 3088 Lo hira:ra 3089 Lo hira:ri 308A Lo hira:ru 308B Lo hira:re 308C Lo hira:ro 308D Lo hira:wasmall 308E Lo hira:wa 308F Lo hira:wi 3090 Lo hira:we 3091 Lo hira:wo 3092 Lo hira:n 3093 Lo hira:vu 3094 Lo hira:kasmall 3095 Lo hira:kesmall 3096 Lo hira:voicedmarkkanacmb 3099 Mn hira:semivoicedmarkkanacmb 309A Mn hira:voicedmarkkana 309B Sk hira:semivoicedmarkkana 309C Sk hira:iterationhiragana 309D Lm hira:voicediterationhiragana 309E Lm hira:digraphyori 309F Lo # Katakana kata:doublehyphenkana 30A0 Pd kata:asmall 30A1 Lo kata:a 30A2 Lo kata:ismall 30A3 Lo kata:i 30A4 Lo kata:usmall 30A5 Lo kata:u 30A6 Lo kata:esmall 30A7 Lo kata:e 30A8 Lo kata:osmall 30A9 Lo kata:o 30AA Lo kata:ka 30AB Lo kata:ga 30AC Lo kata:ki 30AD Lo kata:gi 30AE Lo kata:ku 30AF Lo kata:gu 30B0 Lo kata:ke 30B1 Lo kata:ge 30B2 Lo kata:ko 30B3 Lo kata:go 30B4 Lo kata:sa 30B5 Lo kata:za 30B6 Lo kata:si 30B7 Lo kata:zi 30B8 Lo kata:su 30B9 Lo kata:zu 30BA Lo kata:se 30BB Lo kata:ze 30BC Lo kata:so 30BD Lo kata:zo 30BE Lo kata:ta 30BF Lo kata:da 30C0 Lo kata:ti 30C1 Lo kata:di 30C2 Lo kata:tusmall 30C3 Lo kata:tu 30C4 Lo kata:du 30C5 Lo kata:te 30C6 Lo kata:de 30C7 Lo kata:to 30C8 Lo kata:do 30C9 Lo kata:na 30CA Lo kata:ni 30CB Lo kata:nu 30CC Lo kata:ne 30CD Lo kata:no 30CE Lo kata:ha 30CF Lo kata:ba 30D0 Lo kata:pa 30D1 Lo kata:hi 30D2 Lo kata:bi 30D3 Lo kata:pi 30D4 Lo kata:hu 30D5 Lo kata:bu 30D6 Lo kata:pu 30D7 Lo kata:he 30D8 Lo kata:be 30D9 Lo kata:pe 30DA Lo kata:ho 30DB Lo kata:bo 30DC Lo kata:po 30DD Lo kata:ma 30DE Lo kata:mi 30DF Lo kata:mu 30E0 Lo kata:me 30E1 Lo kata:mo 30E2 Lo kata:yasmall 30E3 Lo kata:ya 30E4 Lo kata:yusmall 30E5 Lo kata:yu 30E6 Lo kata:yosmall 30E7 Lo kata:yo 30E8 Lo kata:ra 30E9 Lo kata:ri 30EA Lo kata:ru 30EB Lo kata:re 30EC Lo kata:ro 30ED Lo kata:wasmall 30EE Lo kata:wa 30EF Lo kata:wi 30F0 Lo kata:we 30F1 Lo kata:wo 30F2 Lo kata:n 30F3 Lo kata:vu 30F4 Lo kata:kasmall 30F5 Lo kata:kesmall 30F6 Lo kata:va 30F7 Lo kata:vi 30F8 Lo kata:ve 30F9 Lo kata:vo 30FA Lo kata:middledot 30FB Po kata:prolongedkana 30FC Lm kata:iteration 30FD Lm kata:voicediteration 30FE Lm kata:digraphkoto 30FF Lo # Bopomofo bopo:b 3105 Lo bopo:p 3106 Lo bopo:m 3107 Lo bopo:f 3108 Lo bopo:d 3109 Lo bopo:t 310A Lo bopo:n 310B Lo bopo:l 310C Lo bopo:g 310D Lo bopo:k 310E Lo bopo:h 310F Lo bopo:j 3110 Lo bopo:q 3111 Lo bopo:x 3112 Lo bopo:zh 3113 Lo bopo:ch 3114 Lo bopo:sh 3115 Lo bopo:r 3116 Lo bopo:z 3117 Lo bopo:c 3118 Lo bopo:s 3119 Lo bopo:a 311A Lo bopo:o 311B Lo bopo:e 311C Lo bopo:eh 311D Lo bopo:ai 311E Lo bopo:ei 311F Lo bopo:au 3120 Lo bopo:ou 3121 Lo bopo:an 3122 Lo bopo:en 3123 Lo bopo:ang 3124 Lo bopo:eng 3125 Lo bopo:er 3126 Lo bopo:i 3127 Lo bopo:u 3128 Lo bopo:iu 3129 Lo bopo:v 312A Lo bopo:ng 312B Lo bopo:gn 312C Lo bopo:ih 312D Lo bopo:owithdotabove 312E Lo bopo:nn 312F Lo # Hangul Compatibility Jamo ko:kiyeok 3131 Lo ko:ssangkiyeok 3132 Lo ko:kiyeoksios 3133 Lo ko:nieun 3134 Lo ko:nieuncieuc 3135 Lo ko:nieunhieuh 3136 Lo ko:tikeut 3137 Lo ko:ssangtikeut 3138 Lo ko:rieul 3139 Lo ko:rieulkiyeok 313A Lo ko:rieulmieum 313B Lo ko:rieulpieup 313C Lo ko:rieulsios 313D Lo ko:rieulthieuth 313E Lo ko:rieulphieuph 313F Lo ko:rieulhieuh 3140 Lo ko:mieum 3141 Lo ko:pieup 3142 Lo ko:ssangpieup 3143 Lo ko:pieupsios 3144 Lo ko:sios 3145 Lo ko:ssangsios 3146 Lo ko:ieung 3147 Lo ko:cieuc 3148 Lo ko:ssangcieuc 3149 Lo ko:chieuch 314A Lo ko:khieukh 314B Lo ko:thieuth 314C Lo ko:phieuph 314D Lo ko:hieuh 314E Lo ko:a 314F Lo ko:ae 3150 Lo ko:ya 3151 Lo ko:yae 3152 Lo ko:eo 3153 Lo ko:e 3154 Lo ko:yeo 3155 Lo ko:ye 3156 Lo ko:o 3157 Lo ko:wa 3158 Lo ko:wae 3159 Lo ko:oe 315A Lo ko:yo 315B Lo ko:u 315C Lo ko:weo 315D Lo ko:we 315E Lo ko:wi 315F Lo ko:yu 3160 Lo ko:eu 3161 Lo ko:yi 3162 Lo ko:i 3163 Lo ko:filler 3164 Lo ko:ssangnieun 3165 Lo ko:nieuntikeut 3166 Lo ko:nieunsios 3167 Lo ko:nieunpansios 3168 Lo ko:rieulkiyeoksios 3169 Lo ko:rieultikeut 316A Lo ko:rieulpieupsios 316B Lo ko:rieulpansios 316C Lo ko:rieulyeorinhieuh 316D Lo ko:mieumpieup 316E Lo ko:mieumsios 316F Lo ko:mieumpansios 3170 Lo ko:kapyeounmieum 3171 Lo ko:pieupkiyeok 3172 Lo ko:pieuptikeut 3173 Lo ko:pieupsioskiyeok 3174 Lo ko:pieupsiostikeut 3175 Lo ko:pieupcieuc 3176 Lo ko:pieupthieuth 3177 Lo ko:kapyeounpieup 3178 Lo ko:kapyeounssangpieup 3179 Lo ko:sioskiyeok 317A Lo ko:siosnieun 317B Lo ko:siostikeut 317C Lo ko:siospieup 317D Lo ko:sioscieuc 317E Lo ko:pansios 317F Lo ko:ssangieung 3180 Lo ko:yesieung 3181 Lo ko:yesieungsios 3182 Lo ko:yesieungpansios 3183 Lo ko:kapyeounphieuph 3184 Lo ko:ssanghieuh 3185 Lo ko:yeorinhieuh 3186 Lo ko:yoya 3187 Lo ko:yoyae 3188 Lo ko:yoi 3189 Lo ko:yuyeo 318A Lo ko:yuye 318B Lo ko:yui 318C Lo ko:araea 318D Lo ko:araeae 318E Lo # Enclosed CJK Letters and Months hangulkiyeokparen 3200 So hangulnieunparen 3201 So hangultikeutparen 3202 So hangulrieulparen 3203 So hangulmieumparen 3204 So hangulpieupparen 3205 So hangulsiosparen 3206 So hangulieungparen 3207 So hangulcieucparen 3208 So hangulchieuchparen 3209 So hangulkhieukhparen 320A So hangulthieuthparen 320B So hangulphieuphparen 320C So hangulhieuhparen 320D So hangulkiyeokaparen 320E So hangulnieunaparen 320F So hangultikeutaparen 3210 So hangulrieulaparen 3211 So hangulmieumaparen 3212 So hangulpieupaparen 3213 So hangulsiosaparen 3214 So hangulieungaparen 3215 So hangulcieucaparen 3216 So hangulchieuchaparen 3217 So hangulkhieukhaparen 3218 So hangulthieuthaparen 3219 So hangulphieuphaparen 321A So hangulhieuhaparen 321B So hangulcieucuparen 321C So ojeonparen 321D So ohuparen 321E So oneideographicparen 3220 No twoideographicparen 3221 No threeideographicparen 3222 No fourideographicparen 3223 No fiveideographicparen 3224 No sixideographicparen 3225 No sevenideographicparen 3226 No eightideographicparen 3227 No nineideographicparen 3228 No tenideographicparen 3229 No moonideographicparen 322A So fireideographicparen 322B So waterideographicparen 322C So woodideographicparen 322D So metalideographicparen 322E So earthideographicparen 322F So sunideographicparen 3230 So stockideographicparen 3231 So haveideographicparen 3232 So societyideographicparen 3233 So nameideographicparen 3234 So specialideographicparen 3235 So financialideographicparen 3236 So congratulationideographicparen 3237 So laborideographicparen 3238 So representideographicparen 3239 So callideographicparen 323A So studyideographicparen 323B So superviseideographicparen 323C So enterpriseideographicparen 323D So resourceideographicparen 323E So allianceideographicparen 323F So festivalideographicparen 3240 So restideographicparen 3241 So selfideographicparen 3242 So reachideographicparen 3243 So questionideographiccircled 3244 So kindergartenideographiccircled 3245 So schoolideographiccircled 3246 So kotoideographiccircled 3247 So tencirclesquare 3248 No twentycirclesquare 3249 No thirtycirclesquare 324A No fortycirclesquare 324B No fiftycirclesquare 324C No sixtycirclesquare 324D No seventycirclesquare 324E No eightycirclesquare 324F No partnership 3250 So twentyonecircle 3251 No twentytwocircle 3252 No twentythreecircle 3253 No twentyfourcircle 3254 No twentyfivecircle 3255 No twentysixcircle 3256 No twentysevencircle 3257 No twentyeightcircle 3258 No twentyninecircle 3259 No thirtycircle 325A No thirtyonecircle 325B No thirtytwocircle 325C No thirtythreecircle 325D No thirtyfourcircle 325E No thirtyfivecircle 325F No kiyeokcirclekorean 3260 So nieuncirclekorean 3261 So tikeutcirclekorean 3262 So rieulcirclekorean 3263 So mieumcirclekorean 3264 So pieupcirclekorean 3265 So sioscirclekorean 3266 So ieungcirclekorean 3267 So cieuccirclekorean 3268 So chieuchcirclekorean 3269 So khieukhcirclekorean 326A So thieuthcirclekorean 326B So phieuphcirclekorean 326C So hieuhcirclekorean 326D So kiyeokacirclekorean 326E So nieunacirclekorean 326F So tikeutacirclekorean 3270 So rieulacirclekorean 3271 So mieumacirclekorean 3272 So pieupacirclekorean 3273 So siosacirclekorean 3274 So ieungacirclekorean 3275 So cieucacirclekorean 3276 So chieuchacirclekorean 3277 So khieukhacirclekorean 3278 So thieuthacirclekorean 3279 So phieuphacirclekorean 327A So hieuhacirclekorean 327B So chamkocircle 327C So jueuicircle 327D So ieungucirclekorean 327E So koreanstandardsymbol 327F So oneideographiccircled 3280 No twoideographiccircled 3281 No threeideographiccircled 3282 No fourideographiccircled 3283 No fiveideographiccircled 3284 No sixideographiccircled 3285 No sevenideographiccircled 3286 No eightideographiccircled 3287 No nineideographiccircled 3288 No tenideographiccircled 3289 No moonideographiccircled 328A So fireideographiccircled 328B So waterideographiccircled 328C So woodideographiccircled 328D So metalideographiccircled 328E So earthideographiccircled 328F So sunideographiccircled 3290 So stockideographiccircled 3291 So haveideographiccircled 3292 So societyideographiccircled 3293 So nameideographiccircled 3294 So specialideographiccircled 3295 So financialideographiccircled 3296 So congratulationideographiccircled 3297 So laborideographiccircled 3298 So secretideographiccircled 3299 So maleideographiccircled 329A So femaleideographiccircled 329B So suitableideographiccircled 329C So excellentideographiccircled 329D So printideographiccircled 329E So attentionideographiccircled 329F So itemideographiccircled 32A0 So restideographiccircled 32A1 So copyideographiccircled 32A2 So correctideographiccircled 32A3 So highideographiccircled 32A4 So centreideographiccircled 32A5 So lowideographiccircled 32A6 So leftideographiccircled 32A7 So rightideographiccircled 32A8 So medicineideographiccircled 32A9 So religionideographiccircled 32AA So studyideographiccircled 32AB So superviseideographiccircled 32AC So enterpriseideographiccircled 32AD So resourceideographiccircled 32AE So allianceideographiccircled 32AF So nightideographiccircled 32B0 So thirtysixcircle 32B1 No thirtysevencircle 32B2 No thirtyeightcircle 32B3 No thirtyninecircle 32B4 No fortycircle 32B5 No fortyonecircle 32B6 No fortytwocircle 32B7 No fortythreecircle 32B8 No fortyfourcircle 32B9 No fortyfivecircle 32BA No fortysixcircle 32BB No fortysevencircle 32BC No fortyeightcircle 32BD No fortyninecircle 32BE No fiftycircle 32BF No januarytelegraph 32C0 So februarytelegraph 32C1 So marchtelegraph 32C2 So apriltelegraph 32C3 So maytelegraph 32C4 So junetelegraph 32C5 So julytelegraph 32C6 So augusttelegraph 32C7 So septembertelegraph 32C8 So octobertelegraph 32C9 So novembertelegraph 32CA So decembertelegraph 32CB So Hgfullwidth 32CC So ergfullwidth 32CD So eVfullwidth 32CE So LTDfullwidth 32CF So acirclekatakana 32D0 So icirclekatakana 32D1 So ucirclekatakana 32D2 So ecirclekatakana 32D3 So ocirclekatakana 32D4 So kacirclekatakana 32D5 So kicirclekatakana 32D6 So kucirclekatakana 32D7 So kecirclekatakana 32D8 So kocirclekatakana 32D9 So sacirclekatakana 32DA So sicirclekatakana 32DB So sucirclekatakana 32DC So secirclekatakana 32DD So socirclekatakana 32DE So tacirclekatakana 32DF So ticirclekatakana 32E0 So tucirclekatakana 32E1 So tecirclekatakana 32E2 So tocirclekatakana 32E3 So nacirclekatakana 32E4 So nicirclekatakana 32E5 So nucirclekatakana 32E6 So necirclekatakana 32E7 So nocirclekatakana 32E8 So hacirclekatakana 32E9 So hicirclekatakana 32EA So hucirclekatakana 32EB So hecirclekatakana 32EC So hocirclekatakana 32ED So macirclekatakana 32EE So micirclekatakana 32EF So mucirclekatakana 32F0 So mecirclekatakana 32F1 So mocirclekatakana 32F2 So yacirclekatakana 32F3 So yucirclekatakana 32F4 So yocirclekatakana 32F5 So racirclekatakana 32F6 So ricirclekatakana 32F7 So rucirclekatakana 32F8 So recirclekatakana 32F9 So rocirclekatakana 32FA So wacirclekatakana 32FB So wicirclekatakana 32FC So wecirclekatakana 32FD So wocirclekatakana 32FE So squareeranamereiwa 32FF So # CJK Compatibility apaatosquare 3300 So aruhuasquare 3301 So anpeasquare 3302 So aarusquare 3303 So iningusquare 3304 So intisquare 3305 So uonsquare 3306 So esukuudosquare 3307 So eekaasquare 3308 So onsusquare 3309 So oomusquare 330A So kairisquare 330B So karattosquare 330C So karoriisquare 330D So garonsquare 330E So ganmasquare 330F So gigasquare 3310 So giniisquare 3311 So kyuriisquare 3312 So girudaasquare 3313 So kirosquare 3314 So kiroguramusquare 3315 So kiromeetorusquare 3316 So kirowattosquare 3317 So guramusquare 3318 So guramutonsquare 3319 So kuruzeirosquare 331A So kuroonesquare 331B So keesusquare 331C So korunasquare 331D So kooposquare 331E So saikurusquare 331F So santiimusquare 3320 So siringusquare 3321 So sentisquare 3322 So sentosquare 3323 So daasusquare 3324 So desisquare 3325 So dorusquare 3326 So tonsquare 3327 So nanosquare 3328 So nottosquare 3329 So haitusquare 332A So paasentosquare 332B So paatusquare 332C So baarerusquare 332D So piasutorusquare 332E So pikurusquare 332F So pikosquare 3330 So birusquare 3331 So huaraddosquare 3332 So huiitosquare 3333 So bussyerusquare 3334 So huransquare 3335 So hekutaarusquare 3336 So pesosquare 3337 So penihisquare 3338 So herutusquare 3339 So pensusquare 333A So peezisquare 333B So beetasquare 333C So pointosquare 333D So borutosquare 333E So honsquare 333F So pondosquare 3340 So hoorusquare 3341 So hoonsquare 3342 So maikurosquare 3343 So mairusquare 3344 So mahhasquare 3345 So marukusquare 3346 So mansyonsquare 3347 So mikuronsquare 3348 So mirisquare 3349 So miribaarusquare 334A So megasquare 334B So megatonsquare 334C So meetorusquare 334D So yaadosquare 334E So yaarusquare 334F So yuansquare 3350 So rittorusquare 3351 So rirasquare 3352 So rupiisquare 3353 So ruuburusquare 3354 So remusquare 3355 So rentogensquare 3356 So wattosquare 3357 So ideographictelegraphsymbolforhourzero 3358 So ideographictelegraphsymbolforhourone 3359 So ideographictelegraphsymbolforhourtwo 335A So ideographictelegraphsymbolforhourthree 335B So ideographictelegraphsymbolforhourfour 335C So ideographictelegraphsymbolforhourfive 335D So ideographictelegraphsymbolforhoursix 335E So ideographictelegraphsymbolforhourseven 335F So ideographictelegraphsymbolforhoureight 3360 So ideographictelegraphsymbolforhournine 3361 So ideographictelegraphsymbolforhourten 3362 So ideographictelegraphsymbolforhoureleven 3363 So ideographictelegraphsymbolforhourtwelve 3364 So ideographictelegraphsymbolforhourthirteen 3365 So ideographictelegraphsymbolforhourfourteen 3366 So ideographictelegraphsymbolforhourfifteen 3367 So ideographictelegraphsymbolforhoursixteen 3368 So ideographictelegraphsymbolforhourseventeen 3369 So ideographictelegraphsymbolforhoureighteen 336A So ideographictelegraphsymbolforhournineteen 336B So ideographictelegraphsymbolforhourtwenty 336C So ideographictelegraphsymbolforhourtwentyone 336D So ideographictelegraphsymbolforhourtwentytwo 336E So ideographictelegraphsymbolforhourtwentythree 336F So ideographictelegraphsymbolforhourtwentyfour 3370 So hpafullwidth 3371 So dafullwidth 3372 So aufullwidth 3373 So barfullwidth 3374 So ovfullwidth 3375 So pcfullwidth 3376 So dmfullwidth 3377 So dm2fullwidth 3378 So dm3fullwidth 3379 So iufullwidth 337A So eranameheiseisquare 337B So eranamesyouwasquare 337C So eranametaisyousquare 337D So eranamemeizisquare 337E So corporationsquare 337F So paampsfullwidth 3380 So nafullwidth 3381 So muafullwidth 3382 So mafullwidth 3383 So kafullwidth 3384 So kbfullwidth 3385 So mbfullwidth 3386 So gbfullwidth 3387 So calfullwidth 3388 So kcalfullwidth 3389 So pffullwidth 338A So nffullwidth 338B So muffullwidth 338C So mugfullwidth 338D So mgfullwidth 338E So kgfullwidth 338F So hzfullwidth 3390 So khzfullwidth 3391 So mhzfullwidth 3392 So ghzfullwidth 3393 So thzfullwidth 3394 So mulfullwidth 3395 So mlfullwidth 3396 So dlfullwidth 3397 So klfullwidth 3398 So fmfullwidth 3399 So nmfullwidth 339A So mumfullwidth 339B So mmfullwidth 339C So cmfullwidth 339D So kmfullwidth 339E So mm2fullwidth 339F So cm2fullwidth 33A0 So m2fullwidth 33A1 So km2fullwidth 33A2 So mm3fullwidth 33A3 So cm3fullwidth 33A4 So m3fullwidth 33A5 So km3fullwidth 33A6 So moversfullwidth 33A7 So movers2fullwidth 33A8 So pafullwidth 33A9 So kpafullwidth 33AA So mpafullwidth 33AB So gpafullwidth 33AC So radfullwidth 33AD So radoversfullwidth 33AE So radovers2fullwidth 33AF So psfullwidth 33B0 So nsfullwidth 33B1 So musfullwidth 33B2 So msfullwidth 33B3 So pvfullwidth 33B4 So nvfullwidth 33B5 So muvfullwidth 33B6 So mvfullwidth 33B7 So kvfullwidth 33B8 So mvmegafullwidth 33B9 So pwfullwidth 33BA So nwfullwidth 33BB So muwfullwidth 33BC So mwfullwidth 33BD So kwfullwidth 33BE So mwmegafullwidth 33BF So kohmfullwidth 33C0 So mohmfullwidth 33C1 So amfullwidth 33C2 So bqfullwidth 33C3 So ccfullwidth 33C4 So cdfullwidth 33C5 So coverkgfullwidth 33C6 So cofullwidth 33C7 So dbfullwidth 33C8 So gyfullwidth 33C9 So hafullwidth 33CA So hpfullwidth 33CB So infullwidth 33CC So kkfullwidth 33CD So kmcapitalfullwidth 33CE So ktfullwidth 33CF So lmfullwidth 33D0 So lnfullwidth 33D1 So logfullwidth 33D2 So lxfullwidth 33D3 So mbsmallfullwidth 33D4 So milfullwidth 33D5 So molfullwidth 33D6 So phfullwidth 33D7 So pmfullwidth 33D8 So ppmfullwidth 33D9 So prfullwidth 33DA So srfullwidth 33DB So svfullwidth 33DC So wbfullwidth 33DD So vovermfullwidth 33DE So aovermfullwidth 33DF So dayonetelegraph 33E0 So daytwotelegraph 33E1 So daythreetelegraph 33E2 So dayfourtelegraph 33E3 So dayfivetelegraph 33E4 So daysixtelegraph 33E5 So dayseventelegraph 33E6 So dayeighttelegraph 33E7 So dayninetelegraph 33E8 So daytentelegraph 33E9 So dayeleventelegraph 33EA So daytwelvetelegraph 33EB So daythirteentelegraph 33EC So dayfourteentelegraph 33ED So dayfifteentelegraph 33EE So daysixteentelegraph 33EF So dayseventeentelegraph 33F0 So dayeighteentelegraph 33F1 So daynineteentelegraph 33F2 So daytwentytelegraph 33F3 So daytwentyonetelegraph 33F4 So daytwentytwotelegraph 33F5 So daytwentythreetelegraph 33F6 So daytwentyfourtelegraph 33F7 So daytwentyfivetelegraph 33F8 So daytwentysixtelegraph 33F9 So daytwentyseventelegraph 33FA So daytwentyeighttelegraph 33FB So daytwentyninetelegraph 33FC So daythirtytelegraph 33FD So daythirtyonetelegraph 33FE So galsquare 33FF So # Latin Extended-D stresstonemod A720 Sk stresslowtonemod A721 Sk Egyptalef A722 Lu egyptalef A723 Ll Egyptain A724 Lu egyptain A725 Ll Heng A726 Lu heng A727 Ll Tz A728 Lu tz A729 Ll Tresillo A72A Lu tresillo A72B Ll Cuatrillo A72C Lu cuatrillo A72D Ll Cuatrillocomma A72E Lu cuatrillocomma A72F Ll Fsmall A730 Ll Ssmall A731 Ll AA A732 Lu aa A733 Ll AO A734 Lu ao A735 Ll AU A736 Lu au A737 Ll AV A738 Lu av A739 Ll AVhorizontalbar A73A Lu avhorizontalbar A73B Ll AY A73C Lu ay A73D Ll Cdotreversed A73E Lu cdotreversed A73F Ll Kstroke A740 Lu kstroke A741 Ll Kdiagonalstroke A742 Lu kdiagonalstroke A743 Ll Kstrokediagonalstroke A744 Lu kstrokediagonalstroke A745 Ll Lbroken A746 Lu lbroken A747 Ll Lstroke A748 Lu lstroke A749 Ll Ostroke A74A Lu ostroke A74B Ll Oloop A74C Lu oloop A74D Ll OO A74E Lu oo A74F Ll Pstrokedescender A750 Lu pstrokedescender A751 Ll Pflourish A752 Lu pflourish A753 Ll Ptail A754 Lu ptail A755 Ll Qstrokedescender A756 Lu qstrokedescender A757 Ll Qdiagonalstroke A758 Lu qdiagonalstroke A759 Ll Rrotunda A75A Lu rrotunda A75B Ll Rumrotunda A75C Lu rumrotunda A75D Ll Vdiagonalstroke A75E Lu vdiagonalstroke A75F Ll Vy A760 Lu vy A761 Ll Visigothicz A762 Lu visigothicz A763 Ll Thornstroke A764 Lu thornstroke A765 Ll Thornstrokedescender A766 Lu thornstrokedescender A767 Ll Vend A768 Lu vend A769 Ll Et A76A Lu et A76B Ll Is A76C Lu is A76D Ll Con A76E Lu con A76F Ll usmod A770 Lm dum A771 Ll lum A772 Ll mum A773 Ll num A774 Ll rum A775 Ll Rumsmall A776 Ll tum A777 Ll um A778 Ll Dinsular A779 Lu dinsular A77A Ll Finsular A77B Lu finsular A77C Ll Ginsular A77D Lu Ginsularturned A77E Lu ginsularturned A77F Ll Lturned A780 Lu lturned A781 Ll Rinsular A782 Lu rinsular A783 Ll Sinsular A784 Lu sinsular A785 Ll Tinsular A786 Lu tinsular A787 Ll circumflexlow A788 Lm colonmod A789 Sk shortequalsmod A78A Sk Saltillo A78B Lu saltillo A78C Ll Hturned A78D Lu lbeltretroflex A78E Ll sinologicaldot A78F Lo Ndescender A790 Lu ndescender A791 Ll Cbar A792 Lu cbar A793 Ll cpalatalhook A794 Ll hpalatalhook A795 Ll Bflourish A796 Lu bflourish A797 Ll Fstroke A798 Lu fstroke A799 Ll Volapukae A79A Lu volapukae A79B Ll Volapukoe A79C Lu volapukoe A79D Ll Volapukue A79E Lu volapukue A79F Ll Gobliquestroke A7A0 Lu gobliquestroke A7A1 Ll Kobliquestroke A7A2 Lu kobliquestroke A7A3 Ll Nobliquestroke A7A4 Lu nobliquestroke A7A5 Ll Robliquestroke A7A6 Lu robliquestroke A7A7 Ll Sobliquestroke A7A8 Lu sobliquestroke A7A9 Ll Hhook A7AA Lu Ereversedopen A7AB Lu Scriptg A7AC Lu Lbelt A7AD Lu Ismall A7AE Lu Qsmall A7AF Ll Kturned A7B0 Lu Tturned A7B1 Lu Jcrossed-tail A7B2 Lu lt:Chi A7B3 Lu lt:Beta A7B4 Lu lt:beta A7B5 Ll lt:Omega A7B6 Lu lt:omega A7B7 Ll Ustroke A7B8 Lu ustroke A7B9 Ll Glottala A7BA Lu glottala A7BB Ll Glottali A7BC Lu glottali A7BD Ll Glottalu A7BE Lu glottalu A7BF Ll Anglicanaw A7C2 Lu anglicanaw A7C3 Ll Cpalatalhook A7C4 Lu Shook A7C5 Lu Zpalatalhook A7C6 Lu iepigraphicsideways A7F7 Lo Hstrokemod A7F8 Lm ligatureoemod A7F9 Lm Mturnedsmall A7FA Ll freversedepigraphic A7FB Lo preversedepigraphic A7FC Lo mepigraphicinverted A7FD Lo iaepigraphic A7FE Lo archaicmepigraphic A7FF Lo # Javanese panyangga A980 Mn cecak A981 Mn layar A982 Mn wignyan A983 Mc java:a A984 Lo ikawi A985 Lo java:i A986 Lo ii A987 Lo java:u A988 Lo pacerek A989 Lo ngalelet A98A Lo ngaleletraswadi A98B Lo java:e A98C Lo ai A98D Lo java:o A98E Lo ka A98F Lo kasasak A990 Lo kamurda A991 Lo ga A992 Lo gamurda A993 Lo nga A994 Lo ca A995 Lo camurda A996 Lo ja A997 Lo nyamurda A998 Lo jamahaprana A999 Lo nya A99A Lo tta A99B Lo ttamahaprana A99C Lo dda A99D Lo ddamahaprana A99E Lo namurda A99F Lo ta A9A0 Lo tamurda A9A1 Lo da A9A2 Lo damahaprana A9A3 Lo na A9A4 Lo pa A9A5 Lo pamurda A9A6 Lo ba A9A7 Lo bamurda A9A8 Lo ma A9A9 Lo ya A9AA Lo ra A9AB Lo raagung A9AC Lo la A9AD Lo wa A9AE Lo samurda A9AF Lo samahaprana A9B0 Lo sa A9B1 Lo ha A9B2 Lo cecaktelu A9B3 Mn tarungvowel A9B4 Mc tolongvowel A9B5 Mc wuluvowel A9B6 Mn wulumelikvowel A9B7 Mn sukuvowel A9B8 Mn sukumendutvowel A9B9 Mn talingvowel A9BA Mc dirgamurevowel A9BB Mc pepetvowel A9BC Mn keretconsonant A9BD Mn pengkalconsonant A9BE Mc cakraconsonant A9BF Mc pangkon A9C0 Mc rerengganleft A9C1 Po rerengganright A9C2 Po andappada A9C3 Po madyapada A9C4 Po luhurpada A9C5 Po windupada A9C6 Po pangkatpada A9C7 Po lingsapada A9C8 Po lungsipada A9C9 Po adegpada A9CA Po adegadegpada A9CB Po piselehpada A9CC Po turnedpiselehpada A9CD Po pangrangkep A9CF Lm java:zero A9D0 Nd java:one A9D1 Nd java:two A9D2 Nd java:three A9D3 Nd java:four A9D4 Nd java:five A9D5 Nd java:six A9D6 Nd java:seven A9D7 Nd java:eight A9D8 Nd java:nine A9D9 Nd tirtatumetespada A9DE Po isen-isenpada A9DF Po # Latin Extended-E redalphabar AB30 Ll areversedschwa AB31 Ll efractur AB32 Ll redebar AB33 Ll eflourish AB34 Ll flenis AB35 Ll gtailscript AB36 Ll llazyinverteds AB37 Ll lmiddledbltilde AB38 Ll lmiddlering AB39 Ll mtail AB3A Ll ntail AB3B Ll engtail AB3C Ll ofractur AB3D Ll ofracturstroke AB3E Ll ostrokeopen AB3F Ll oeinverted AB40 Ll oestroketurned AB41 Ll oehorizontalstroketurned AB42 Ll ooopenturned AB43 Ll ooopenstroketurned AB44 Ll stirrupr AB45 Ll Rrightlegsmall AB46 Ll routhandle AB47 Ll rdbl AB48 Ll rtail AB49 Ll rtaildbl AB4A Ll rscript AB4B Ll rringscript AB4C Ll baselineesh AB4D Ll urightlegshort AB4E Ll urightlegbarshort AB4F Ll ui AB50 Ll uiturned AB51 Ll ulefthook AB52 Ll lt:chi AB53 Ll lt:chilowrightring AB54 Ll lt:chilowleftserif AB55 Ll xlowrightring AB56 Ll xlongleftleg AB57 Ll xlongleftlegandlowrightring AB58 Ll xlongleftlegserif AB59 Ll yrightlegshort AB5A Ll breveinvertedmod AB5B Sk hengmod AB5C Lm llazyinvertedsmod AB5D Lm lmiddletildemod AB5E Lm ulefthookmod AB5F Lm sakhayat AB60 Ll iotifiede AB61 Ll oeopen AB62 Ll uo AB63 Ll alphainverted AB64 Ll lt:Omegasmall AB65 Ll dzdigraphretroflexhook AB66 Ll tsdigraphretroflexhook AB67 Ll # Cherokee Supplement chrk:asmall AB70 Ll chrk:esmall AB71 Ll chrk:ismall AB72 Ll chrk:osmall AB73 Ll chrk:usmall AB74 Ll chrk:vsmall AB75 Ll chrk:gasmall AB76 Ll chrk:kasmall AB77 Ll chrk:gesmall AB78 Ll chrk:gismall AB79 Ll chrk:gosmall AB7A Ll chrk:gusmall AB7B Ll chrk:gvsmall AB7C Ll chrk:hasmall AB7D Ll chrk:hesmall AB7E Ll chrk:hismall AB7F Ll chrk:hosmall AB80 Ll chrk:husmall AB81 Ll chrk:hvsmall AB82 Ll chrk:lasmall AB83 Ll chrk:lesmall AB84 Ll chrk:lismall AB85 Ll chrk:losmall AB86 Ll chrk:lusmall AB87 Ll chrk:lvsmall AB88 Ll chrk:masmall AB89 Ll chrk:mesmall AB8A Ll chrk:mismall AB8B Ll chrk:mosmall AB8C Ll chrk:musmall AB8D Ll chrk:nasmall AB8E Ll chrk:hnasmall AB8F Ll chrk:nahsmall AB90 Ll chrk:nesmall AB91 Ll chrk:nismall AB92 Ll chrk:nosmall AB93 Ll chrk:nusmall AB94 Ll chrk:nvsmall AB95 Ll chrk:quasmall AB96 Ll chrk:quesmall AB97 Ll chrk:quismall AB98 Ll chrk:quosmall AB99 Ll chrk:quusmall AB9A Ll chrk:quvsmall AB9B Ll chrk:sasmall AB9C Ll chrk:ssmall AB9D Ll chrk:sesmall AB9E Ll chrk:sismall AB9F Ll chrk:sosmall ABA0 Ll chrk:susmall ABA1 Ll chrk:svsmall ABA2 Ll chrk:dasmall ABA3 Ll chrk:tasmall ABA4 Ll chrk:desmall ABA5 Ll chrk:tesmall ABA6 Ll chrk:dismall ABA7 Ll chrk:tismall ABA8 Ll chrk:dosmall ABA9 Ll chrk:dusmall ABAA Ll chrk:dvsmall ABAB Ll chrk:dlasmall ABAC Ll chrk:tlasmall ABAD Ll chrk:tlesmall ABAE Ll chrk:tlismall ABAF Ll chrk:tlosmall ABB0 Ll chrk:tlusmall ABB1 Ll chrk:tlvsmall ABB2 Ll chrk:tsasmall ABB3 Ll chrk:tsesmall ABB4 Ll chrk:tsismall ABB5 Ll chrk:tsosmall ABB6 Ll chrk:tsusmall ABB7 Ll chrk:tsvsmall ABB8 Ll chrk:wasmall ABB9 Ll chrk:wesmall ABBA Ll chrk:wismall ABBB Ll chrk:wosmall ABBC Ll chrk:wusmall ABBD Ll chrk:wvsmall ABBE Ll chrk:yasmall ABBF Ll # Private Use Area # Alphabetic Presentation Forms f_f FB00 Ll fi FB01 Ll fl FB02 Ll f_f_i FB03 Ll f_f_l FB04 Ll longs_t FB05 Ll s_t FB06 Ll men_nowarmn FB13 Ll men_echarmn FB14 Ll men_iniarmn FB15 Ll vew_nowarmn FB16 Ll men_xeharmn FB17 Ll yodwithhiriq:hb FB1D Lo varikajudeospanish:hb FB1E Mn yod_yod_patah:hb FB1F Lo ayinalt:hb FB20 Lo alefwide:hb FB21 Lo daletwide:hb FB22 Lo hewide:hb FB23 Lo kafwide:hb FB24 Lo lamedwide:hb FB25 Lo finalmemwide:hb FB26 Lo reshwide:hb FB27 Lo tavwide:hb FB28 Lo plussignalt:hb FB29 Sm shinwithshinDot:hb FB2A Lo shinwithsinDot:hb FB2B Lo shinwithdageshandshinDot:hb FB2C Lo shinwithdageshandsinDot:hb FB2D Lo alefwithpatah:hb FB2E Lo alefwithqamats:hb FB2F Lo alefwithmapiq:hb FB30 Lo betwithdagesh:hb FB31 Lo gimelwithdagesh:hb FB32 Lo daletwithdagesh:hb FB33 Lo hewithmapiq:hb FB34 Lo vavwithdagesh:hb FB35 Lo zayinwithdagesh:hb FB36 Lo tetwithdagesh:hb FB38 Lo yodwithdagesh:hb FB39 Lo finalkafwithdagesh:hb FB3A Lo kafwithdagesh:hb FB3B Lo lamedwithdagesh:hb FB3C Lo memwithdagesh:hb FB3E Lo nunwithdagesh:hb FB40 Lo samekhwithdagesh:hb FB41 Lo finalpewithdagesh:hb FB43 Lo pewithdagesh:hb FB44 Lo tsadiwithdagesh:hb FB46 Lo qofwithdagesh:hb FB47 Lo reshwithdagesh:hb FB48 Lo shinwithdagesh:hb FB49 Lo tavwithdagesh:hb FB4A Lo vavwithholam:hb FB4B Lo betwithrafe:hb FB4C Lo kafwithrafe:hb FB4D Lo pewithrafe:hb FB4E Lo ligaturealeflamed:hb FB4F Lo # Arabic Presentation Forms-A alefwasla.isol FB50 Lo alefwasla.fina FB51 Lo beeh.isol FB52 Lo beeh.fina FB53 Lo beeh.init FB54 Lo beeh.medi FB55 Lo peh.isol FB56 Lo peh.fina FB57 Lo peh.init FB58 Lo peh.medi FB59 Lo beheh.isol FB5A Lo beheh.fina FB5B Lo beheh.init FB5C Lo beheh.medi FB5D Lo tteheh.isol FB5E Lo tteheh.fina FB5F Lo tteheh.init FB60 Lo tteheh.medi FB61 Lo teheh.isol FB62 Lo teheh.fina FB63 Lo teheh.init FB64 Lo teheh.medi FB65 Lo tteh.isol FB66 Lo tteh.fina FB67 Lo tteh.init FB68 Lo tteh.medi FB69 Lo veh.isol FB6A Lo veh.fina FB6B Lo veh.init FB6C Lo veh.medi FB6D Lo peheh.isol FB6E Lo peheh.fina FB6F Lo peheh.init FB70 Lo peheh.medi FB71 Lo dyeh.isol FB72 Lo dyeh.fina FB73 Lo dyeh.init FB74 Lo dyeh.medi FB75 Lo nyeh.isol FB76 Lo nyeh.fina FB77 Lo nyeh.init FB78 Lo nyeh.medi FB79 Lo tcheh.isol FB7A Lo tcheh.fina FB7B Lo tcheh.init FB7C Lo tcheh.medi FB7D Lo tcheheh.isol FB7E Lo tcheheh.fina FB7F Lo tcheheh.init FB80 Lo tcheheh.medi FB81 Lo ddahal.isol FB82 Lo ddahal.fina FB83 Lo dahal.isol FB84 Lo dahal.fina FB85 Lo dul.isol FB86 Lo dul.fina FB87 Lo ddal.isol FB88 Lo ddal.fina FB89 Lo jeh.isol FB8A Lo jeh.fina FB8B Lo rreh.isol FB8C Lo rreh.fina FB8D Lo keheh.isol FB8E Lo keheh.fina FB8F Lo keheh.init FB90 Lo keheh.medi FB91 Lo gaf.isol FB92 Lo gaf.fina FB93 Lo gaf.init FB94 Lo gaf.medi FB95 Lo gueh.isol FB96 Lo gueh.fina FB97 Lo gueh.init FB98 Lo gueh.medi FB99 Lo ngoeh.isol FB9A Lo ngoeh.fina FB9B Lo ngoeh.init FB9C Lo ngoeh.medi FB9D Lo noonghunna.isol FB9E Lo noonghunna.fina FB9F Lo rnoon.isol FBA0 Lo rnoon.fina FBA1 Lo rnoon.init FBA2 Lo rnoon.medi FBA3 Lo hehyeh.isol FBA4 Lo hehyeh.fina FBA5 Lo hehgoal.isol FBA6 Lo hehgoal.fina FBA7 Lo hehgoal.init FBA8 Lo hehgoal.medi FBA9 Lo hehdoachashmee.isol FBAA Lo hehdoachashmee.fina FBAB Lo hehdoachashmee.init FBAC Lo hehdoachashmee.medi FBAD Lo yehbarree.isol FBAE Lo yehbarree.fina FBAF Lo yehbarreehamza.isol FBB0 Lo yehbarreehamza.fina FBB1 Lo symboldotabove FBB2 Sk symboldotbelow FBB3 Sk symboltwodotsabove FBB4 Sk symboltwodotsbelow FBB5 Sk symbolabovethreedotsabove FBB6 Sk symbolbelowthreedotsabove FBB7 Sk symbolpointingabovedownthreedotsabove FBB8 Sk symbolpointingbelowdownthreedotsabove FBB9 Sk symbolfourdotsabove FBBA Sk symbolfourdotsbelow FBBB Sk symboldoubleverticalbarbelow FBBC Sk symboltwodotsverticallyabove FBBD Sk symboltwodotsverticallybelow FBBE Sk symbolring FBBF Sk symboltahabovesmall FBC0 Sk symboltahbelowsmall FBC1 Sk ng.isol FBD3 Lo ng.fina FBD4 Lo ng.init FBD5 Lo ng.medi FBD6 Lo u.isol FBD7 Lo u.fina FBD8 Lo oe.isol FBD9 Lo oe.fina FBDA Lo yu.isol FBDB Lo yu.fina FBDC Lo uhamza.isol FBDD Lo ve.isol FBDE Lo ve.fina FBDF Lo oekirghiz.isol FBE0 Lo oekirghiz.fina FBE1 Lo yukirghiz.isol FBE2 Lo yukirghiz.fina FBE3 Lo e.isol FBE4 Lo e.fina FBE5 Lo e.init FBE6 Lo e.medi FBE7 Lo uighurkazakhkirghizalefmaksura.init FBE8 Lo uighurkazakhkirghizalefmaksura.medi FBE9 Lo yeh.init_hamzaabove.medi_alef.fina FBEA Lo yeh.medi_hamzaabove.medi_alef.fina FBEB Lo yeh.init_hamzaabove.medi_ae.fina FBEC Lo yeh.medi_hamzaabove.medi_ae.fina FBED Lo yeh.init_hamzaabove.medi_waw.fina FBEE Lo yeh.medi_hamzaabove.medi_waw.fina FBEF Lo yeh.init_hamzaabove.medi_u.fina FBF0 Lo yeh.medi_hamzaabove.medi_u.fina FBF1 Lo yeh.init_hamzaabove.medi_oe.fina FBF2 Lo yeh.medi_hamzaabove.medi_oe.fina FBF3 Lo yeh.init_hamzaabove.medi_yu.fina FBF4 Lo yeh.medi_hamzaabove.medi_yu.fina FBF5 Lo yeh.init_hamzaabove.medi_e.fina FBF6 Lo yeh.medi_hamzaabove.medi_e.fina FBF7 Lo yeh.init_hamzaabove.medi_e.medi FBF8 Lo uighurkirghizyeh.init_hamzaabove.medi_alefmaksura.fina FBF9 Lo uighurkirghizyeh.medi_hamzaabove.medi_alefmaksura.fina FBFA Lo uighurkirghizyeh.init_hamzaabove.medi_alefmaksura.medi FBFB Lo yehfarsi.isol FBFC Lo yehfarsi.fina FBFD Lo yehfarsi.init FBFE Lo yehfarsi.medi FBFF Lo yeh.init_hamzaabove.medi_jeem.fina FC00 Lo yeh.init_hamzaabove.medi_hah.fina FC01 Lo yeh.init_hamzaabove.medi_meem.fina FC02 Lo yeh.init_hamzaabove.medi_alefmaksura.fina FC03 Lo yeh.init_hamzaabove.medi_yeh.fina FC04 Lo beh.init_jeem.fina FC05 Lo beh.init_hah.fina FC06 Lo beh.init_khah.fina FC07 Lo beh.init_meem.fina FC08 Lo beh.init_alefmaksura.fina FC09 Lo beh.init_yeh.fina FC0A Lo teh.init_jeem.fina FC0B Lo teh.init_hah.fina FC0C Lo teh.init_khah.fina FC0D Lo teh.init_meem.fina FC0E Lo teh.init_alefmaksura.fina FC0F Lo teh.init_yeh.fina FC10 Lo theh.init_jeem.fina FC11 Lo theh.init_meem.fina FC12 Lo theh.init_alefmaksura.fina FC13 Lo theh.init_yeh.fina FC14 Lo jeem.init_hah.fina FC15 Lo jeem.init_meem.fina FC16 Lo hah.init_jeem.fina FC17 Lo hah.init_meem.fina FC18 Lo khah.init_jeem.fina FC19 Lo khah.init_hah.fina FC1A Lo khah.init_meem.fina FC1B Lo seen.init_jeem.fina FC1C Lo seen.init_hah.fina FC1D Lo seen.init_khah.fina FC1E Lo seen.init_meem.fina FC1F Lo sad.init_hah.fina FC20 Lo sad.init_meem.fina FC21 Lo dad.init_jeem.fina FC22 Lo dad.init_hah.fina FC23 Lo dad.init_khah.fina FC24 Lo dad.init_meem.fina FC25 Lo tah.init_hah.fina FC26 Lo tah.init_meem.fina FC27 Lo zah.init_meem.fina FC28 Lo ain.init_jeem.fina FC29 Lo ain.init_meem.fina FC2A Lo ghain.init_jeem.fina FC2B Lo ghain.init_meem.fina FC2C Lo feh.init_jeem.fina FC2D Lo feh.init_hah.fina FC2E Lo feh.init_khah.fina FC2F Lo feh.init_meem.fina FC30 Lo feh.init_alefmaksura.fina FC31 Lo feh.init_yeh.fina FC32 Lo qaf.init_hah.fina FC33 Lo qaf.init_meem.fina FC34 Lo qaf.init_alefmaksura.fina FC35 Lo qaf.init_yeh.fina FC36 Lo kaf.init_alef.fina FC37 Lo kaf.init_jeem.fina FC38 Lo kaf.init_hah.fina FC39 Lo kaf.init_khah.fina FC3A Lo kaf.init_lam.fina FC3B Lo kaf.init_meem.fina FC3C Lo kaf.init_alefmaksura.fina FC3D Lo kaf.init_yeh.fina FC3E Lo lam.init_jeem.fina FC3F Lo lam.init_hah.fina FC40 Lo lam.init_khah.fina FC41 Lo lam.init_meem.fina FC42 Lo lam.init_alefmaksura.fina FC43 Lo lam.init_yeh.fina FC44 Lo meem.init_jeem.fina FC45 Lo meem.init_hah.fina FC46 Lo meem.init_khah.fina FC47 Lo meem.init_meem.fina FC48 Lo meem.init_alefmaksura.fina FC49 Lo meem.init_yeh.fina FC4A Lo noon.init_jeem.fina FC4B Lo noon.init_hah.fina FC4C Lo noon.init_khah.fina FC4D Lo noon.init_meem.fina FC4E Lo noon.init_alefmaksura.fina FC4F Lo noon.init_yeh.fina FC50 Lo heh.init_jeem.fina FC51 Lo heh.init_meem.fina FC52 Lo heh.init_alefmaksura.fina FC53 Lo heh.init_yeh.fina FC54 Lo yeh.init_jeem.fina FC55 Lo yeh.init_hah.fina FC56 Lo yeh.init_khah.fina FC57 Lo yeh.init_meem.fina FC58 Lo yeh.init_alefmaksura.fina FC59 Lo yeh.init_yeh.fina FC5A Lo thal.init_superscriptalef.fina FC5B Lo reh.init_superscriptalef.fina FC5C Lo alefmaksura.init_superscriptalef.fina FC5D Lo shaddaDammatanIsol FC5E Lo shaddaKasratanIsol FC5F Lo shaddaFathaIsol FC60 Lo shaddaDammaIsol FC61 Lo shaddaKasraIsol FC62 Lo shaddaAlefIsol FC63 Lo yeh.medi_hamzaabove.medi_reh.fina FC64 Lo yeh.medi_hamzaabove.medi_zain.fina FC65 Lo yeh.medi_hamzaabove.medi_meem.fina FC66 Lo yeh.medi_hamzaabove.medi_noon.fina FC67 Lo yeh.medi_hamzaabove.medi_alefmaksura.fina FC68 Lo yeh.medi_hamzaabove.medi_yeh.fina FC69 Lo beh.medi_reh.fina FC6A Lo beh.medi_zain.fina FC6B Lo beh.medi_meem.fina FC6C Lo beh.medi_noon.fina FC6D Lo beh.medi_alefmaksura.fina FC6E Lo beh.medi_yeh.fina FC6F Lo teh.medi_reh.fina FC70 Lo teh.medi_zain.fina FC71 Lo teh.medi_meem.fina FC72 Lo teh.medi_noon.fina FC73 Lo teh.medi_alefmaksura.fina FC74 Lo teh.medi_yeh.fina FC75 Lo theh.medi_reh.fina FC76 Lo theh.medi_zain.fina FC77 Lo theh.medi_meem.fina FC78 Lo theh.medi_noon.fina FC79 Lo theh.medi_alefmaksura.fina FC7A Lo theh.medi_yeh.fina FC7B Lo feh.medi_alefmaksura.fina FC7C Lo feh.medi_yeh.fina FC7D Lo qaf.medi_alefmaksura.fina FC7E Lo qaf.medi_yeh.fina FC7F Lo kaf.medi_alef.fina FC80 Lo kaf.medi_lam.fina FC81 Lo kaf.medi_meem.fina FC82 Lo kaf.medi_alefmaksura.fina FC83 Lo kaf.medi_yeh.fina FC84 Lo lam.medi_meem.fina FC85 Lo lam.medi_alefmaksura.fina FC86 Lo lam.medi_yeh.fina FC87 Lo meem.medi_alef.fina FC88 Lo meem.medi_meem.fina FC89 Lo noon.medi_reh.fina FC8A Lo noon.medi_zain.fina FC8B Lo noon.medi_meem.fina FC8C Lo noon.medi_noon.fina FC8D Lo noon.medi_alefmaksura.fina FC8E Lo noon.medi_yeh.fina FC8F Lo alefmaksura.medi_superscriptalef.fina FC90 Lo yeh.medi_reh.fina FC91 Lo yeh.medi_zain.fina FC92 Lo yeh.medi_meem.fina FC93 Lo yeh.medi_noon.fina FC94 Lo yeh.medi_alefmaksura.fina FC95 Lo yeh.medi_yeh.fina FC96 Lo yeh.init_hamzaabove.medi_jeem.medi FC97 Lo yeh.init_hamzaabove.medi_hah.medi FC98 Lo yeh.init_hamzaabove.medi_khah.medi FC99 Lo yeh.init_hamzaabove.medi_meem.medi FC9A Lo yeh.init_hamzaabove.medi_heh.medi FC9B Lo beh.init_jeem.medi FC9C Lo beh.init_hah.medi FC9D Lo beh.init_khah.medi FC9E Lo beh.init_meem.medi FC9F Lo beh.init_heh.medi FCA0 Lo teh.init_jeem.medi FCA1 Lo teh.init_hah.medi FCA2 Lo teh.init_khah.medi FCA3 Lo teh.init_meem.medi FCA4 Lo teh.init_heh.medi FCA5 Lo theh.init_meem.medi FCA6 Lo jeem.init_hah.medi FCA7 Lo jeem.init_meem.medi FCA8 Lo hah.init_jeem.medi FCA9 Lo hah.init_meem.medi FCAA Lo khah.init_jeem.medi FCAB Lo khah.init_meem.medi FCAC Lo seen.init_jeem.medi FCAD Lo seen.init_hah.medi FCAE Lo seen.init_khah.medi FCAF Lo seen.init_meem.medi FCB0 Lo sad.init_hah.medi FCB1 Lo sad.init_khah.medi FCB2 Lo sad.init_meem.medi FCB3 Lo dad.init_jeem.medi FCB4 Lo dad.init_hah.medi FCB5 Lo dad.init_khah.medi FCB6 Lo dad.init_meem.medi FCB7 Lo tah.init_hah.medi FCB8 Lo zah.init_meem.medi FCB9 Lo ain.init_jeem.medi FCBA Lo ain.init_meem.medi FCBB Lo ghain.init_jeem.medi FCBC Lo ghain.init_meem.medi FCBD Lo feh.init_jeem.medi FCBE Lo feh.init_hah.medi FCBF Lo feh.init_khah.medi FCC0 Lo feh.init_meem.medi FCC1 Lo qaf.init_hah.medi FCC2 Lo qaf.init_meem.medi FCC3 Lo kaf.init_jeem.medi FCC4 Lo kaf.init_hah.medi FCC5 Lo kaf.init_khah.medi FCC6 Lo kaf.init_lam.medi FCC7 Lo kaf.init_meem.medi FCC8 Lo lam.init_jeem.medi FCC9 Lo lam.init_hah.medi FCCA Lo lam.init_khah.medi FCCB Lo lam.init_meem.medi FCCC Lo lam.init_heh.medi FCCD Lo meem.init_jeem.medi FCCE Lo meem.init_hah.medi FCCF Lo meem.init_khah.medi FCD0 Lo meem.init_meem.medi FCD1 Lo noon.init_jeem.medi FCD2 Lo noon.init_hah.medi FCD3 Lo noon.init_khah.medi FCD4 Lo noon.init_meem.medi FCD5 Lo noon.init_heh.medi FCD6 Lo heh.init_jeem.medi FCD7 Lo heh.init_meem.medi FCD8 Lo heh.init_superscriptalef.medi FCD9 Lo yeh.init_jeem.medi FCDA Lo yeh.init_hah.medi FCDB Lo yeh.init_khah.medi FCDC Lo yeh.init_meem.medi FCDD Lo yeh.init_heh.medi FCDE Lo yeh.medi_hamzaabove.medi_meem.medi FCDF Lo yeh.medi_hamzaabove.medi_heh.medi FCE0 Lo beh.medi_meem.medi FCE1 Lo beh.medi_heh.medi FCE2 Lo teh.medi_meem.medi FCE3 Lo teh.medi_heh.medi FCE4 Lo theh.medi_meem.medi FCE5 Lo theh.medi_heh.medi FCE6 Lo seen.medi_meem.medi FCE7 Lo seen.medi_heh.medi FCE8 Lo sheen.medi_meem.medi FCE9 Lo sheen.medi_heh.medi FCEA Lo kaf.medi_lam.medi FCEB Lo kaf.medi_meem.medi FCEC Lo lam.medi_meem.medi FCED Lo noon.medi_meem.medi FCEE Lo noon.medi_heh.medi FCEF Lo yeh.medi_meem.medi FCF0 Lo yeh.medi_heh.medi FCF1 Lo shaddaFathaMedi FCF2 Lo shaddaDammaMedi FCF3 Lo shaddaKasraMedi FCF4 Lo tah.init_alefmaksura.fina FCF5 Lo tah.init_yeh.fina FCF6 Lo ain.init_alefmaksura.fina FCF7 Lo ain.init_yeh.fina FCF8 Lo ghain.init_alefmaksura.fina FCF9 Lo ghain.init_yeh.fina FCFA Lo seen.init_alefmaksura.fina FCFB Lo seen.init_yeh.fina FCFC Lo sheen.init_alefmaksura.fina FCFD Lo sheen.init_yeh.fina FCFE Lo hah.init_alefmaksura.fina FCFF Lo hah.init_yeh.fina FD00 Lo jeem.init_alefmaksura.fina FD01 Lo jeem.init_yeh.fina FD02 Lo khah.init_alefmaksura.fina FD03 Lo khah.init_yeh.fina FD04 Lo sad.init_alefmaksura.fina FD05 Lo sad.init_yeh.fina FD06 Lo dad.init_alefmaksura.fina FD07 Lo dad.init_yeh.fina FD08 Lo sheen.init_jeem.fina FD09 Lo sheen.init_hah.fina FD0A Lo sheen.init_khah.fina FD0B Lo sheen.init_meem.fina FD0C Lo sheen.init_reh.fina FD0D Lo seen.init_reh.fina FD0E Lo sad.init_reh.fina FD0F Lo dad.init_reh.fina FD10 Lo tah.medi_alefmaksura.fina FD11 Lo tah.medi_yeh.fina FD12 Lo ain.medi_alefmaksura.fina FD13 Lo ain.medi_yeh.fina FD14 Lo ghain.medi_alefmaksura.fina FD15 Lo ghain.medi_yeh.fina FD16 Lo seen.medi_alefmaksura.fina FD17 Lo seen.medi_yeh.fina FD18 Lo sheen.medi_alefmaksura.fina FD19 Lo sheen.medi_yeh.fina FD1A Lo hah.medi_alefmaksura.fina FD1B Lo hah.medi_yeh.fina FD1C Lo jeem.medi_alefmaksura.fina FD1D Lo jeem.medi_yeh.fina FD1E Lo khah.medi_alefmaksura.fina FD1F Lo khah.medi_yeh.fina FD20 Lo sad.medi_alefmaksura.fina FD21 Lo sad.medi_yeh.fina FD22 Lo dad.medi_alefmaksura.fina FD23 Lo dad.medi_yeh.fina FD24 Lo sheen.medi_jeem.fina FD25 Lo sheen.medi_hah.fina FD26 Lo sheen.medi_khah.fina FD27 Lo sheen.medi_meem.fina FD28 Lo sheen.medi_reh.fina FD29 Lo seen.medi_reh.fina FD2A Lo sad.medi_reh.fina FD2B Lo dad.medi_reh.fina FD2C Lo sheen.init_jeem.medi FD2D Lo sheen.init_hah.medi FD2E Lo sheen.init_khah.medi FD2F Lo sheen.init_meem.medi FD30 Lo seen.init_heh.medi FD31 Lo sheen.init_heh.medi FD32 Lo tah.init_meem.medi FD33 Lo seen.medi_jeem.medi FD34 Lo seen.medi_hah.medi FD35 Lo seen.medi_khah.medi FD36 Lo sheen.medi_jeem.medi FD37 Lo sheen.medi_hah.medi FD38 Lo sheen.medi_khah.medi FD39 Lo tah.medi_meem.medi FD3A Lo zah.medi_meem.medi FD3B Lo alef.medi_fathatan.fina FD3C Lo alef.init_fathatan.fina FD3D Lo ornateleftparenthesis FD3E Pe ornaterightparenthesis FD3F Ps teh.init_jeem.medi_meem.medi FD50 Lo teh.medi_hah.medi_jeem.fina FD51 Lo teh.init_hah.medi_jeem.medi FD52 Lo teh.init_hah.medi_meem.medi FD53 Lo teh.init_khah.medi_meem.medi FD54 Lo teh.init_meem.medi_jeem.medi FD55 Lo teh.init_meem.medi_hah.medi FD56 Lo teh.init_meem.medi_khah.medi FD57 Lo jeem.medi_meem.medi_hah.fina FD58 Lo jeem.init_meem.medi_hah.medi FD59 Lo hah.medi_meem.medi_yeh.fina FD5A Lo hah.medi_meem.medi_alefmaksura.fina FD5B Lo seen.init_hah.medi_jeem.medi FD5C Lo seen.init_jeem.medi_hah.medi FD5D Lo seen.medi_jeem.medi_alefmaksura.fina FD5E Lo seen.medi_meem.medi_hah.fina FD5F Lo seen.init_meem.medi_hah.medi FD60 Lo seen.init_meem.medi_jeem.medi FD61 Lo seen.medi_meem.medi_meem.fina FD62 Lo seen.init_meem.medi_meem.medi FD63 Lo sad.medi_hah.medi_hah.fina FD64 Lo sad.init_hah.medi_hah.medi FD65 Lo sad.medi_meem.medi_meem.fina FD66 Lo sheen.medi_hah.medi_meem.fina FD67 Lo sheen.init_hah.medi_meem.medi FD68 Lo sheen.medi_jeem.medi_yeh.fina FD69 Lo sheen.medi_meem.medi_khah.fina FD6A Lo sheen.init_meem.medi_khah.medi FD6B Lo sheen.medi_meem.medi_meem.fina FD6C Lo sheen.init_meem.medi_meem.medi FD6D Lo dad.medi_hah.medi_alefmaksura.fina FD6E Lo dad.medi_khah.medi_meem.fina FD6F Lo dad.init_khah.medi_meem.medi FD70 Lo tah.medi_meem.medi_hah.fina FD71 Lo tah.init_meem.medi_hah.medi FD72 Lo tah.init_meem.medi_meem.medi FD73 Lo tah.medi_meem.medi_yeh.fina FD74 Lo ain.medi_jeem.medi_meem.fina FD75 Lo ain.medi_meem.medi_meem.fina FD76 Lo ain.init_meem.medi_meem.medi FD77 Lo ain.medi_meem.medi_alefmaksura.fina FD78 Lo ghain.medi_meem.medi_meem.fina FD79 Lo ghain.medi_meem.medi_yeh.fina FD7A Lo ghain.medi_meem.medi_alefmaksura.fina FD7B Lo feh.medi_khah.medi_meem.fina FD7C Lo feh.init_khah.medi_meem.medi FD7D Lo qaf.medi_meem.medi_hah.fina FD7E Lo qaf.medi_meem.medi_meem.fina FD7F Lo lam.medi_hah.medi_meem.fina FD80 Lo lam.medi_hah.medi_yeh.fina FD81 Lo lam.medi_hah.medi_alefmaksura.fina FD82 Lo lam.init_jeem.medi_jeem.medi FD83 Lo lam.medi_jeem.medi_jeem.fina FD84 Lo lam.medi_khah.medi_meem.fina FD85 Lo lam.init_khah.medi_meem.medi FD86 Lo lam.medi_meem.medi_hah.fina FD87 Lo lam.init_meem.medi_hah.medi FD88 Lo meem.init_hah.medi_jeem.medi FD89 Lo meem.init_hah.medi_meem.medi FD8A Lo meem.medi_hah.medi_yeh.fina FD8B Lo meem.init_jeem.medi_hah.medi FD8C Lo meem.init_jeem.medi_meem.medi FD8D Lo meem.init_khah.medi_jeem.medi FD8E Lo meem.init_khah.medi_meem.medi FD8F Lo meem.init_jeem.medi_khah.medi FD92 Lo heh.init_meem.medi_jeem.medi FD93 Lo heh.init_meem.medi_meem.medi FD94 Lo noon.init_hah.medi_meem.medi FD95 Lo noon.medi_hah.medi_alefmaksura.fina FD96 Lo noon.medi_jeem.medi_meem.fina FD97 Lo noon.init_jeem.medi_meem.medi FD98 Lo noon.medi_jeem.medi_alefmaksura.fina FD99 Lo noon.medi_meem.medi_yeh.fina FD9A Lo noon.medi_meem.medi_alefmaksura.fina FD9B Lo yeh.medi_meem.medi_meem.fina FD9C Lo yeh.init_meem.medi_meem.medi FD9D Lo beh.medi_khah.medi_yeh.fina FD9E Lo teh.medi_jeem.medi_yeh.fina FD9F Lo teh.medi_jeem.medi_alefmaksura.fina FDA0 Lo teh.medi_khah.medi_yeh.fina FDA1 Lo teh.medi_khah.medi_alefmaksura.fina FDA2 Lo teh.medi_meem.medi_yeh.fina FDA3 Lo teh.medi_meem.medi_alefmaksura.fina FDA4 Lo jeem.medi_meem.medi_yeh.fina FDA5 Lo jeem.medi_hah.medi_alefmaksura.fina FDA6 Lo jeem.medi_meem.medi_alefmaksura.fina FDA7 Lo seen.medi_khah.medi_alefmaksura.fina FDA8 Lo sad.medi_hah.medi_yeh.fina FDA9 Lo sheen.medi_hah.medi_yeh.fina FDAA Lo dad.medi_hah.medi_yeh.fina FDAB Lo lam.medi_jeem.medi_yeh.fina FDAC Lo lam.medi_meem.medi_yeh.fina FDAD Lo yeh.medi_hah.medi_yeh.fina FDAE Lo yeh.medi_jeem.medi_yeh.fina FDAF Lo yeh.medi_meem.medi_yeh.fina FDB0 Lo meem.medi_meem.medi_yeh.fina FDB1 Lo qaf.medi_meem.medi_yeh.fina FDB2 Lo noon.medi_hah.medi_yeh.fina FDB3 Lo qaf.init_meem.medi_hah.medi FDB4 Lo lam.init_hah.medi_meem.medi FDB5 Lo ain.medi_meem.medi_yeh.fina FDB6 Lo kaf.medi_meem.medi_yeh.fina FDB7 Lo noon.init_jeem.medi_hah.medi FDB8 Lo meem.medi_khah.medi_yeh.fina FDB9 Lo lam.init_jeem.medi_meem.medi FDBA Lo kaf.medi_meem.medi_meem.fina FDBB Lo lam.medi_jeem.medi_meem.fina FDBC Lo noon.medi_jeem.medi_hah.fina FDBD Lo jeem.medi_hah.medi_yeh.fina FDBE Lo hah.medi_jeem.medi_yeh.fina FDBF Lo meem.medi_jeem.medi_yeh.fina FDC0 Lo feh.medi_meem.medi_yeh.fina FDC1 Lo beh.medi_hah.medi_yeh.fina FDC2 Lo kaf.init_meem.medi_meem.medi FDC3 Lo ain.init_jeem.medi_meem.medi FDC4 Lo sad.init_meem.medi_meem.medi FDC5 Lo seen.medi_khah.medi_yeh.fina FDC6 Lo noon.medi_jeem.medi_yeh.fina FDC7 Lo SallaUsedAsKoranicStopSign FDF0 Lo QalaUsedAsKoranicStopSign FDF1 Lo Allah FDF2 Lo Akbar FDF3 Lo Mohammad FDF4 Lo Salam FDF5 Lo Rasoul FDF6 Lo Alayhe FDF7 Lo Wasallam FDF8 Lo Salla FDF9 Lo SallallahouAlayheWasallam FDFA Lo Jallajalalouhou FDFB Lo rial FDFC Sc BismillahArRahmanArRaheem FDFD So # Vertical Forms vert:comma FE10 Po vert:ideographiccomma FE11 Po vert:ideographicfullstop FE12 Po vert:colon FE13 Po vert:semicolon FE14 Po vert:exclam FE15 Po vert:question FE16 Po vert:bracketwhiteleft FE17 Ps vert:brakcetwhiteright FE18 Pe vert:ellipsishor FE19 Po # CJK Compatibility Forms twodotleadervertical FE30 Po emdashvertical FE31 Pd endashvertical FE32 Pd underscorevertical FE33 Pc underscorewavyvertical FE34 Pc parenleftvertical FE35 Ps parenrightvertical FE36 Pe braceleftvertical FE37 Ps bracerightvertical FE38 Pe tortoiseshellbracketleftvertical FE39 Ps tortoiseshellbracketrightvertical FE3A Pe blacklenticularbracketleftvertical FE3B Ps blacklenticularbracketrightvertical FE3C Pe dblanglebracketleftvertical FE3D Ps dblanglebracketrightvertical FE3E Pe anglebracketleftvertical FE3F Ps anglebracketrightvertical FE40 Pe cornerbracketleftvertical FE41 Ps cornerbracketrightvertical FE42 Pe whitecornerbracketleftvertical FE43 Ps whitecornerbracketrightvertical FE44 Pe sesamedot FE45 Po whitesesamedot FE46 Po squarebracketleftvertical FE47 Ps squarebracketrightvertical FE48 Pe overlinedashed FE49 Po overlinecenterline FE4A Po overlinewavy FE4B Po overlinedblwavy FE4C Po underscoredashed FE4D Pc underscorecenterline FE4E Pc underscorewavy FE4F Pc # Small Form Variants commasmall FE50 Po ideographiccommasmall FE51 Po periodsmall FE52 Po semicolonsmall FE54 Po colonsmall FE55 Po questionsmall FE56 Po exclamsmall FE57 Po emdashsmall FE58 Pd parenthesisleftsmall FE59 Ps parenthesisrightsmall FE5A Pe braceleftsmall FE5B Ps bracerightsmall FE5C Pe tortoiseshellbracketleftsmall FE5D Ps tortoiseshellbracketrightsmall FE5E Pe numbersignsmall FE5F Po ampersandsmall FE60 Po asterisksmall FE61 Po plussmall FE62 Sm hyphensmall FE63 Pd lesssmall FE64 Sm greatersmall FE65 Sm equalsmall FE66 Sm backslashsmall FE68 Po dollarsmall FE69 Sc percentsmall FE6A Po commercialatsmall FE6B Po # Arabic Presentation Forms-B fathatanIsol FE70 Lo tatweelFathatanAbove FE71 Lo dammatanIsol FE72 Lo kashidaFina FE73 Lo kasratanIsol FE74 Lo fathaIsol FE76 Lo fathaMedi FE77 Lo dammaIsol FE78 Lo dammaMedi FE79 Lo kasraIsol FE7A Lo kasraMedi FE7B Lo shaddaIsol FE7C Lo shaddaMedi FE7D Lo sukunIsol FE7E Lo sukunMedi FE7F Lo hamzaIsol FE80 Lo alefmadda.isol FE81 Lo alefmadda.fina FE82 Lo alefhamza.isol FE83 Lo alefhamza.fina FE84 Lo wawhamza.isol FE85 Lo wawhamza.fina FE86 Lo alefhamzabelow.isol FE87 Lo alefhamzabelow.fina FE88 Lo yehhamza.isol FE89 Lo yehhamza.fina FE8A Lo yehhamza.init FE8B Lo yehhamza.medi FE8C Lo alef.isol FE8D Lo alef.fina FE8E Lo beh.isol FE8F Lo beh.fina FE90 Lo beh.init FE91 Lo beh.medi FE92 Lo tehmarbuta.isol FE93 Lo tehmarbuta.fina FE94 Lo teh.isol FE95 Lo teh.fina FE96 Lo teh.init FE97 Lo teh.medi FE98 Lo theh.isol FE99 Lo theh.fina FE9A Lo theh.init FE9B Lo theh.medi FE9C Lo jeem.isol FE9D Lo jeem.fina FE9E Lo jeem.init FE9F Lo jeem.medi FEA0 Lo hah.isol FEA1 Lo hah.fina FEA2 Lo hah.init FEA3 Lo hah.medi FEA4 Lo khah.isol FEA5 Lo khah.fina FEA6 Lo khah.init FEA7 Lo khah.medi FEA8 Lo dal.isol FEA9 Lo dal.fina FEAA Lo thal.isol FEAB Lo thal.fina FEAC Lo reh.isol FEAD Lo reh.fina FEAE Lo zain.isol FEAF Lo zain.fina FEB0 Lo seen.isol FEB1 Lo seen.fina FEB2 Lo seen.init FEB3 Lo seen.medi FEB4 Lo sheen.isol FEB5 Lo sheen.fina FEB6 Lo sheen.init FEB7 Lo sheen.medi FEB8 Lo sad.isol FEB9 Lo sad.fina FEBA Lo sad.init FEBB Lo sad.medi FEBC Lo dad.isol FEBD Lo dad.fina FEBE Lo dad.init FEBF Lo dad.medi FEC0 Lo tah.isol FEC1 Lo tah.fina FEC2 Lo tah.init FEC3 Lo tah.medi FEC4 Lo zah.isol FEC5 Lo zah.fina FEC6 Lo zah.init FEC7 Lo zah.medi FEC8 Lo ain.isol FEC9 Lo ain.fina FECA Lo ain.init FECB Lo ain.medi FECC Lo ghain.isol FECD Lo ghain.fina FECE Lo ghain.init FECF Lo ghain.medi FED0 Lo feh.isol FED1 Lo feh.fina FED2 Lo feh.init FED3 Lo feh.medi FED4 Lo qaf.isol FED5 Lo qaf.fina FED6 Lo qaf.init FED7 Lo qaf.medi FED8 Lo kaf.isol FED9 Lo kaf.fina FEDA Lo kaf.init FEDB Lo kaf.medi FEDC Lo lam.isol FEDD Lo lam.fina FEDE Lo lam.init FEDF Lo lam.medi FEE0 Lo meem.isol FEE1 Lo meem.fina FEE2 Lo meem.init FEE3 Lo meem.medi FEE4 Lo noon.isol FEE5 Lo noon.fina FEE6 Lo noon.init FEE7 Lo noon.medi FEE8 Lo heh.isol FEE9 Lo heh.fina FEEA Lo heh.init FEEB Lo heh.medi FEEC Lo waw.isol FEED Lo waw.fina FEEE Lo alefmaksura.isol FEEF Lo alefmaksura.fina FEF0 Lo yeh.isol FEF1 Lo yeh.fina FEF2 Lo yeh.init FEF3 Lo yeh.medi FEF4 Lo lam.init_alef.medi_maddaabove.fina FEF5 Lo lam.medi_alef.medi_maddaabove.fina FEF6 Lo lam.init_alef.medi_hamzaabove.fina FEF7 Lo lam.medi_alef.medi_hamzaabove.fina FEF8 Lo lam.init_alef.medi_hamzabelow.fina FEF9 Lo lam.medi_alef.medi_hamzabelow.fina FEFA Lo lam.init_alef.fina FEFB Lo lam.medi_alef.fina FEFC Lo zerowidthnobreakspace FEFF Cf # Halfwidth and Fullwidth Forms fwd:exclam FF01 Po fwd:quotedbl FF02 Po fwd:numbersign FF03 Po fwd:dollar FF04 Sc fwd:percent FF05 Po fwd:ampersand FF06 Po fwd:quotesingle FF07 Po fwd:parenthesisleft FF08 Ps fwd:parenthesisright FF09 Pe fwd:asterisk FF0A Po fwd:plus FF0B Sm fwd:comma FF0C Po fwd:hyphen FF0D Pd fwd:period FF0E Po fwd:slash FF0F Po fwd:zero FF10 Nd fwd:one FF11 Nd fwd:two FF12 Nd fwd:three FF13 Nd fwd:four FF14 Nd fwd:five FF15 Nd fwd:six FF16 Nd fwd:seven FF17 Nd fwd:eight FF18 Nd fwd:nine FF19 Nd fwd:colon FF1A Po fwd:semicolon FF1B Po fwd:less FF1C Sm fwd:equal FF1D Sm fwd:greater FF1E Sm fwd:question FF1F Po fwd:at FF20 Po fwd:A FF21 Lu fwd:B FF22 Lu fwd:C FF23 Lu fwd:D FF24 Lu fwd:E FF25 Lu fwd:F FF26 Lu fwd:G FF27 Lu fwd:H FF28 Lu fwd:I FF29 Lu fwd:J FF2A Lu fwd:K FF2B Lu fwd:L FF2C Lu fwd:M FF2D Lu fwd:N FF2E Lu fwd:O FF2F Lu fwd:P FF30 Lu fwd:Q FF31 Lu fwd:R FF32 Lu fwd:S FF33 Lu fwd:T FF34 Lu fwd:U FF35 Lu fwd:V FF36 Lu fwd:W FF37 Lu fwd:X FF38 Lu fwd:Y FF39 Lu fwd:Z FF3A Lu fwd:bracketleft FF3B Ps fwd:backslash FF3C Po fwd:bracketright FF3D Pe fwd:asciicircum FF3E Sk fwd:underscore FF3F Pc fwd:grave FF40 Sk fwd:a FF41 Ll fwd:b FF42 Ll fwd:c FF43 Ll fwd:d FF44 Ll fwd:e FF45 Ll fwd:f FF46 Ll fwd:g FF47 Ll fwd:h FF48 Ll fwd:i FF49 Ll fwd:j FF4A Ll fwd:k FF4B Ll fwd:l FF4C Ll fwd:m FF4D Ll fwd:n FF4E Ll fwd:o FF4F Ll fwd:p FF50 Ll fwd:q FF51 Ll fwd:r FF52 Ll fwd:s FF53 Ll fwd:t FF54 Ll fwd:u FF55 Ll fwd:v FF56 Ll fwd:w FF57 Ll fwd:x FF58 Ll fwd:y FF59 Ll fwd:z FF5A Ll fwd:braceleft FF5B Ps fwd:bar FF5C Sm fwd:braceright FF5D Pe fwd:asciitilde FF5E Sm fwd:leftwhiteparenthesis FF5F Ps fwd:rightwhiteparenthesis FF60 Pe hwd:ideographicfullstop FF61 Po hwd:leftcornerbracket FF62 Ps hwd:rightcornerbracket FF63 Pe hwd:ideographiccomma FF64 Po hwd:kata:middledot FF65 Po hwd:kata:wo FF66 Lo hwd:kata:asmall FF67 Lo hwd:kata:ismall FF68 Lo hwd:kata:usmall FF69 Lo hwd:kata:esmall FF6A Lo hwd:kata:osmall FF6B Lo hwd:kata:yasmall FF6C Lo hwd:kata:yusmall FF6D Lo hwd:kata:yosmall FF6E Lo hwd:kata:tusmall FF6F Lo hwd:kata:prolongedkana FF70 Lm hwd:kata:a FF71 Lo hwd:kata:i FF72 Lo hwd:kata:u FF73 Lo hwd:kata:e FF74 Lo hwd:kata:o FF75 Lo hwd:kata:ka FF76 Lo hwd:kata:ki FF77 Lo hwd:kata:ku FF78 Lo hwd:kata:ke FF79 Lo hwd:kata:ko FF7A Lo hwd:kata:sa FF7B Lo hwd:kata:si FF7C Lo hwd:kata:su FF7D Lo hwd:kata:se FF7E Lo hwd:kata:so FF7F Lo hwd:kata:ta FF80 Lo hwd:kata:ti FF81 Lo hwd:kata:tu FF82 Lo hwd:kata:te FF83 Lo hwd:kata:to FF84 Lo hwd:kata:na FF85 Lo hwd:kata:ni FF86 Lo hwd:kata:nu FF87 Lo hwd:kata:ne FF88 Lo hwd:kata:no FF89 Lo hwd:kata:ha FF8A Lo hwd:kata:hi FF8B Lo hwd:kata:hu FF8C Lo hwd:kata:he FF8D Lo hwd:kata:ho FF8E Lo hwd:kata:ma FF8F Lo hwd:kata:mi FF90 Lo hwd:kata:mu FF91 Lo hwd:kata:me FF92 Lo hwd:kata:mo FF93 Lo hwd:kata:ya FF94 Lo hwd:kata:yu FF95 Lo hwd:kata:yo FF96 Lo hwd:kata:ra FF97 Lo hwd:kata:ri FF98 Lo hwd:kata:ru FF99 Lo hwd:kata:re FF9A Lo hwd:kata:ro FF9B Lo hwd:kata:wa FF9C Lo hwd:kata:n FF9D Lo hwd:kata:voiced FF9E Lm hwd:kata:semi-voiced FF9F Lm hwd:hangulfiller FFA0 Lo hwd:kiyeok FFA1 Lo hwd:ssangkiyeok FFA2 Lo hwd:kiyeoksios FFA3 Lo hwd:nieun FFA4 Lo hwd:nieuncieuc FFA5 Lo hwd:nieunhieuh FFA6 Lo hwd:tikeut FFA7 Lo hwd:ssangtikeut FFA8 Lo hwd:rieul FFA9 Lo hwd:rieulkiyeok FFAA Lo hwd:rieulmieum FFAB Lo hwd:rieulpieup FFAC Lo hwd:rieulsios FFAD Lo hwd:rieulthieuth FFAE Lo hwd:rieulphieuph FFAF Lo hwd:rieulhieuh FFB0 Lo hwd:mieum FFB1 Lo hwd:pieup FFB2 Lo hwd:ssangpieup FFB3 Lo hwd:pieupsios FFB4 Lo hwd:sios FFB5 Lo hwd:ssangsios FFB6 Lo hwd:ieung FFB7 Lo hwd:cieuc FFB8 Lo hwd:ssangcieuc FFB9 Lo hwd:chieuch FFBA Lo hwd:khieukh FFBB Lo hwd:thieuth FFBC Lo hwd:phieuph FFBD Lo hwd:hieuh FFBE Lo hwd:a FFC2 Lo hwd:ae FFC3 Lo hwd:ya FFC4 Lo hwd:yae FFC5 Lo hwd:eo FFC6 Lo hwd:e FFC7 Lo hwd:yeo FFCA Lo hwd:ye FFCB Lo hwd:o FFCC Lo hwd:wa FFCD Lo hwd:wae FFCE Lo hwd:oe FFCF Lo hwd:yo FFD2 Lo hwd:u FFD3 Lo hwd:weo FFD4 Lo hwd:we FFD5 Lo hwd:wi FFD6 Lo hwd:yu FFD7 Lo hwd:eu FFDA Lo hwd:yi FFDB Lo hwd:i FFDC Lo fwd:centsign FFE0 Sc fwd:poundsign FFE1 Sc fwd:notsign FFE2 Sm fwd:macron FFE3 Sk fwd:brokenbar FFE4 So fwd:yensign FFE5 Sc fwd:wonsign FFE6 Sc hwd:formslightvertical FFE8 So hwd:leftwardsarrow FFE9 Sm hwd:upwardsarrow FFEA Sm hwd:rightwardsarrow FFEB Sm hwd:downwardsarrow FFEC Sm hwd:blacksquare FFED So hwd:whitecircle FFEE So # Specials interlinearanchor FFF9 Cf interlinearseparator FFFA Cf interlinearterminator FFFB Cf replacementcharobj FFFC So replacementchar FFFD So # Zanabazar Square zanb:a 11A00 Lo zanb:vowelsigni 11A01 Mn zanb:vowelsignue 11A02 Mn zanb:vowelsignu 11A03 Mn zanb:vowelsigne 11A04 Mn zanb:vowelsignoe 11A05 Mn zanb:vowelsigno 11A06 Mn zanb:vowelsignai 11A07 Mn zanb:vowelsignau 11A08 Mn zanb:vowelsignreversedi 11A09 Mn zanb:vowellengthmark 11A0A Mn zanb:ka 11A0B Lo zanb:kha 11A0C Lo zanb:ga 11A0D Lo zanb:gha 11A0E Lo zanb:nga 11A0F Lo zanb:ca 11A10 Lo zanb:cha 11A11 Lo zanb:ja 11A12 Lo zanb:nya 11A13 Lo zanb:tta 11A14 Lo zanb:ttha 11A15 Lo zanb:dda 11A16 Lo zanb:ddha 11A17 Lo zanb:nna 11A18 Lo zanb:ta 11A19 Lo zanb:tha 11A1A Lo zanb:da 11A1B Lo zanb:dha 11A1C Lo zanb:na 11A1D Lo zanb:pa 11A1E Lo zanb:pha 11A1F Lo zanb:ba 11A20 Lo zanb:bha 11A21 Lo zanb:ma 11A22 Lo zanb:tsa 11A23 Lo zanb:tsha 11A24 Lo zanb:dza 11A25 Lo zanb:dzha 11A26 Lo zanb:zha 11A27 Lo zanb:za 11A28 Lo zanb:dashA 11A29 Lo zanb:ya 11A2A Lo zanb:ra 11A2B Lo zanb:la 11A2C Lo zanb:va 11A2D Lo zanb:sha 11A2E Lo zanb:ssa 11A2F Lo zanb:sa 11A30 Lo zanb:ha 11A31 Lo zanb:kssa 11A32 Lo zanb:finalconsonantmark 11A33 Mn zanb:signvirama 11A34 Mn zanb:signcandrabindu 11A35 Mn zanb:signcandrabinduwithornament 11A36 Mn zanb:signcandrawithornament 11A37 Mn zanb:signanusvara 11A38 Mn zanb:signvisarga 11A39 Mc zanb:raclusterinit 11A3A Lo zanb:yaclusterfina 11A3B Mn zanb:raclusterfina 11A3C Mn zanb:laclusterfina 11A3D Mn zanb:vaclusterfina 11A3E Mn zanb:initialheadmark 11A3F Po zanb:closingheadmark 11A40 Po zanb:marktsheg 11A41 Po zanb:markshad 11A42 Po zanb:markdoubleshad 11A43 Po zanb:marklongtsheg 11A44 Po zanb:initialheadmarkdbllined 11A45 Po zanb:closingheadmarkdbllined 11A46 Po zanb:subjoiner 11A47 Mn # Mathematical Alphanumeric Symbols Abold 1D400 Lu Bbold 1D401 Lu Cbold 1D402 Lu Dbold 1D403 Lu Ebold 1D404 Lu Fbold 1D405 Lu Gbold 1D406 Lu Hbold 1D407 Lu Ibold 1D408 Lu Jbold 1D409 Lu Kbold 1D40A Lu Lbold 1D40B Lu Mbold 1D40C Lu Nbold 1D40D Lu Obold 1D40E Lu Pbold 1D40F Lu Qbold 1D410 Lu Rbold 1D411 Lu Sbold 1D412 Lu Tbold 1D413 Lu Ubold 1D414 Lu Vbold 1D415 Lu Wbold 1D416 Lu Xbold 1D417 Lu Ybold 1D418 Lu Zbold 1D419 Lu abold 1D41A Ll bbold 1D41B Ll cbold 1D41C Ll dbold 1D41D Ll ebold 1D41E Ll fbold 1D41F Ll gbold 1D420 Ll hbold 1D421 Ll ibold 1D422 Ll jbold 1D423 Ll kbold 1D424 Ll lbold 1D425 Ll mbold 1D426 Ll nbold 1D427 Ll obold 1D428 Ll pbold 1D429 Ll qbold 1D42A Ll rbold 1D42B Ll sbold 1D42C Ll tbold 1D42D Ll ubold 1D42E Ll vbold 1D42F Ll wbold 1D430 Ll xbold 1D431 Ll ybold 1D432 Ll zbold 1D433 Ll Aitalic 1D434 Lu Bitalic 1D435 Lu Citalic 1D436 Lu Ditalic 1D437 Lu Eitalic 1D438 Lu Fitalic 1D439 Lu Gitalic 1D43A Lu Hitalic 1D43B Lu Iitalic 1D43C Lu Jitalic 1D43D Lu Kitalic 1D43E Lu Litalic 1D43F Lu Mitalic 1D440 Lu Nitalic 1D441 Lu Oitalic 1D442 Lu Pitalic 1D443 Lu Qitalic 1D444 Lu Ritalic 1D445 Lu Sitalic 1D446 Lu Titalic 1D447 Lu Uitalic 1D448 Lu Vitalic 1D449 Lu Witalic 1D44A Lu Xitalic 1D44B Lu Yitalic 1D44C Lu Zitalic 1D44D Lu aitalic 1D44E Ll bitalic 1D44F Ll citalic 1D450 Ll ditalic 1D451 Ll eitalic 1D452 Ll fitalic 1D453 Ll gitalic 1D454 Ll iitalic 1D456 Ll jitalic 1D457 Ll kitalic 1D458 Ll litalic 1D459 Ll mitalic 1D45A Ll nitalic 1D45B Ll oitalic 1D45C Ll pitalic 1D45D Ll qitalic 1D45E Ll ritalic 1D45F Ll sitalic 1D460 Ll titalic 1D461 Ll uitalic 1D462 Ll vitalic 1D463 Ll witalic 1D464 Ll xitalic 1D465 Ll yitalic 1D466 Ll zitalic 1D467 Ll Abolditalic 1D468 Lu Bbolditalic 1D469 Lu Cbolditalic 1D46A Lu Dbolditalic 1D46B Lu Ebolditalic 1D46C Lu Fbolditalic 1D46D Lu Gbolditalic 1D46E Lu Hbolditalic 1D46F Lu Ibolditalic 1D470 Lu Jbolditalic 1D471 Lu Kbolditalic 1D472 Lu Lbolditalic 1D473 Lu Mbolditalic 1D474 Lu Nbolditalic 1D475 Lu Obolditalic 1D476 Lu Pbolditalic 1D477 Lu Qbolditalic 1D478 Lu Rbolditalic 1D479 Lu Sbolditalic 1D47A Lu Tbolditalic 1D47B Lu Ubolditalic 1D47C Lu Vbolditalic 1D47D Lu Wbolditalic 1D47E Lu Xbolditalic 1D47F Lu Ybolditalic 1D480 Lu Zbolditalic 1D481 Lu abolditalic 1D482 Ll bbolditalic 1D483 Ll cbolditalic 1D484 Ll dbolditalic 1D485 Ll ebolditalic 1D486 Ll fbolditalic 1D487 Ll gbolditalic 1D488 Ll hbolditalic 1D489 Ll ibolditalic 1D48A Ll jbolditalic 1D48B Ll kbolditalic 1D48C Ll lbolditalic 1D48D Ll mbolditalic 1D48E Ll nbolditalic 1D48F Ll obolditalic 1D490 Ll pbolditalic 1D491 Ll qbolditalic 1D492 Ll rbolditalic 1D493 Ll sbolditalic 1D494 Ll tbolditalic 1D495 Ll ubolditalic 1D496 Ll vbolditalic 1D497 Ll wbolditalic 1D498 Ll xbolditalic 1D499 Ll ybolditalic 1D49A Ll zbolditalic 1D49B Ll Ascript 1D49C Lu Cscript 1D49E Lu Dscript 1D49F Lu Gscript 1D4A2 Lu Jscript 1D4A5 Lu Kscript 1D4A6 Lu Nscript 1D4A9 Lu Oscript 1D4AA Lu Pscript 1D4AB Lu Qscript 1D4AC Lu Sscript 1D4AE Lu Tscript 1D4AF Lu Uscript 1D4B0 Lu Vscript 1D4B1 Lu Wscript 1D4B2 Lu Xscript 1D4B3 Lu Yscript 1D4B4 Lu Zscript 1D4B5 Lu ascript 1D4B6 Ll bscript 1D4B7 Ll cscript 1D4B8 Ll dscript 1D4B9 Ll fscript 1D4BB Ll hscript 1D4BD Ll iscript 1D4BE Ll jscript 1D4BF Ll kscript 1D4C0 Ll lscript 1D4C1 Ll mscript 1D4C2 Ll nscript 1D4C3 Ll pscript 1D4C5 Ll qscript 1D4C6 Ll math:rscript 1D4C7 Ll sscript 1D4C8 Ll tscript 1D4C9 Ll uscript 1D4CA Ll vscript 1D4CB Ll wscript 1D4CC Ll xscript 1D4CD Ll yscript 1D4CE Ll zscript 1D4CF Ll Aboldscript 1D4D0 Lu Bboldscript 1D4D1 Lu Cboldscript 1D4D2 Lu Dboldscript 1D4D3 Lu Eboldscript 1D4D4 Lu Fboldscript 1D4D5 Lu Gboldscript 1D4D6 Lu Hboldscript 1D4D7 Lu Iboldscript 1D4D8 Lu Jboldscript 1D4D9 Lu Kboldscript 1D4DA Lu Lboldscript 1D4DB Lu Mboldscript 1D4DC Lu Nboldscript 1D4DD Lu Oboldscript 1D4DE Lu Pboldscript 1D4DF Lu Qboldscript 1D4E0 Lu Rboldscript 1D4E1 Lu Sboldscript 1D4E2 Lu Tboldscript 1D4E3 Lu Uboldscript 1D4E4 Lu Vboldscript 1D4E5 Lu Wboldscript 1D4E6 Lu Xboldscript 1D4E7 Lu Yboldscript 1D4E8 Lu Zboldscript 1D4E9 Lu aboldscript 1D4EA Ll bboldscript 1D4EB Ll cboldscript 1D4EC Ll dboldscript 1D4ED Ll eboldscript 1D4EE Ll fboldscript 1D4EF Ll gboldscript 1D4F0 Ll hboldscript 1D4F1 Ll iboldscript 1D4F2 Ll jboldscript 1D4F3 Ll kboldscript 1D4F4 Ll lboldscript 1D4F5 Ll mboldscript 1D4F6 Ll nboldscript 1D4F7 Ll oboldscript 1D4F8 Ll pboldscript 1D4F9 Ll qboldscript 1D4FA Ll rboldscript 1D4FB Ll sboldscript 1D4FC Ll tboldscript 1D4FD Ll uboldscript 1D4FE Ll vboldscript 1D4FF Ll wboldscript 1D500 Ll xboldscript 1D501 Ll yboldscript 1D502 Ll zboldscript 1D503 Ll Afraktur 1D504 Lu Bfraktur 1D505 Lu Dfraktur 1D507 Lu Efraktur 1D508 Lu Ffraktur 1D509 Lu Gfraktur 1D50A Lu Jfraktur 1D50D Lu Kfraktur 1D50E Lu Lfraktur 1D50F Lu Mfraktur 1D510 Lu Nfraktur 1D511 Lu Ofraktur 1D512 Lu Pfraktur 1D513 Lu Qfraktur 1D514 Lu Sfraktur 1D516 Lu Tfraktur 1D517 Lu Ufraktur 1D518 Lu Vfraktur 1D519 Lu Wfraktur 1D51A Lu Xfraktur 1D51B Lu Yfraktur 1D51C Lu afraktur 1D51E Ll bfraktur 1D51F Ll cfraktur 1D520 Ll dfraktur 1D521 Ll efraktur 1D522 Ll ffraktur 1D523 Ll gfraktur 1D524 Ll hfraktur 1D525 Ll ifraktur 1D526 Ll jfraktur 1D527 Ll kfraktur 1D528 Ll lfraktur 1D529 Ll mfraktur 1D52A Ll nfraktur 1D52B Ll ofraktur 1D52C Ll pfraktur 1D52D Ll qfraktur 1D52E Ll rfraktur 1D52F Ll sfraktur 1D530 Ll tfraktur 1D531 Ll ufraktur 1D532 Ll vfraktur 1D533 Ll wfraktur 1D534 Ll xfraktur 1D535 Ll yfraktur 1D536 Ll zfraktur 1D537 Ll Adblstruck 1D538 Lu Bdblstruck 1D539 Lu Ddblstruck 1D53B Lu Edblstruck 1D53C Lu Fdblstruck 1D53D Lu Gdblstruck 1D53E Lu Idblstruck 1D540 Lu Jdblstruck 1D541 Lu Kdblstruck 1D542 Lu Ldblstruck 1D543 Lu Mdblstruck 1D544 Lu Odblstruck 1D546 Lu Sdblstruck 1D54A Lu Tdblstruck 1D54B Lu Udblstruck 1D54C Lu Vdblstruck 1D54D Lu Wdblstruck 1D54E Lu Xdblstruck 1D54F Lu Ydblstruck 1D550 Lu adblstruck 1D552 Ll bdblstruck 1D553 Ll cdblstruck 1D554 Ll ddblstruck 1D555 Ll edblstruck 1D556 Ll fdblstruck 1D557 Ll gdblstruck 1D558 Ll hdblstruck 1D559 Ll idblstruck 1D55A Ll jdblstruck 1D55B Ll kdblstruck 1D55C Ll ldblstruck 1D55D Ll mdblstruck 1D55E Ll ndblstruck 1D55F Ll odblstruck 1D560 Ll pdblstruck 1D561 Ll qdblstruck 1D562 Ll rdblstruck 1D563 Ll sdblstruck 1D564 Ll tdblstruck 1D565 Ll udblstruck 1D566 Ll vdblstruck 1D567 Ll wdblstruck 1D568 Ll xdblstruck 1D569 Ll ydblstruck 1D56A Ll zdblstruck 1D56B Ll Aboldfraktur 1D56C Lu Bboldfraktur 1D56D Lu Cboldfraktur 1D56E Lu Dboldfraktur 1D56F Lu Eboldfraktur 1D570 Lu Fboldfraktur 1D571 Lu Gboldfraktur 1D572 Lu Hboldfraktur 1D573 Lu Iboldfraktur 1D574 Lu Jboldfraktur 1D575 Lu Kboldfraktur 1D576 Lu Lboldfraktur 1D577 Lu Mboldfraktur 1D578 Lu Nboldfraktur 1D579 Lu Oboldfraktur 1D57A Lu Pboldfraktur 1D57B Lu Qboldfraktur 1D57C Lu Rboldfraktur 1D57D Lu Sboldfraktur 1D57E Lu Tboldfraktur 1D57F Lu Uboldfraktur 1D580 Lu Vboldfraktur 1D581 Lu Wboldfraktur 1D582 Lu Xboldfraktur 1D583 Lu Yboldfraktur 1D584 Lu Zboldfraktur 1D585 Lu aboldfraktur 1D586 Ll bboldfraktur 1D587 Ll cboldfraktur 1D588 Ll dboldfraktur 1D589 Ll eboldfraktur 1D58A Ll fboldfraktur 1D58B Ll gboldfraktur 1D58C Ll hboldfraktur 1D58D Ll iboldfraktur 1D58E Ll jboldfraktur 1D58F Ll kboldfraktur 1D590 Ll lboldfraktur 1D591 Ll mboldfraktur 1D592 Ll nboldfraktur 1D593 Ll oboldfraktur 1D594 Ll pboldfraktur 1D595 Ll qboldfraktur 1D596 Ll rboldfraktur 1D597 Ll sboldfraktur 1D598 Ll tboldfraktur 1D599 Ll uboldfraktur 1D59A Ll vboldfraktur 1D59B Ll wboldfraktur 1D59C Ll xboldfraktur 1D59D Ll yboldfraktur 1D59E Ll zboldfraktur 1D59F Ll Asans 1D5A0 Lu Bsans 1D5A1 Lu Csans 1D5A2 Lu Dsans 1D5A3 Lu Esans 1D5A4 Lu Fsans 1D5A5 Lu Gsans 1D5A6 Lu Hsans 1D5A7 Lu Isans 1D5A8 Lu Jsans 1D5A9 Lu Ksans 1D5AA Lu Lsans 1D5AB Lu Msans 1D5AC Lu Nsans 1D5AD Lu Osans 1D5AE Lu Psans 1D5AF Lu Qsans 1D5B0 Lu Rsans 1D5B1 Lu Ssans 1D5B2 Lu Tsans 1D5B3 Lu Usans 1D5B4 Lu Vsans 1D5B5 Lu Wsans 1D5B6 Lu Xsans 1D5B7 Lu Ysans 1D5B8 Lu Zsans 1D5B9 Lu asans 1D5BA Ll bsans 1D5BB Ll csans 1D5BC Ll dsans 1D5BD Ll esans 1D5BE Ll fsans 1D5BF Ll gsans 1D5C0 Ll hsans 1D5C1 Ll isans 1D5C2 Ll jsans 1D5C3 Ll ksans 1D5C4 Ll lsans 1D5C5 Ll msans 1D5C6 Ll nsans 1D5C7 Ll osans 1D5C8 Ll psans 1D5C9 Ll qsans 1D5CA Ll rsans 1D5CB Ll ssans 1D5CC Ll tsans 1D5CD Ll usans 1D5CE Ll vsans 1D5CF Ll wsans 1D5D0 Ll xsans 1D5D1 Ll ysans 1D5D2 Ll zsans 1D5D3 Ll Aboldsans 1D5D4 Lu Bboldsans 1D5D5 Lu Cboldsans 1D5D6 Lu Dboldsans 1D5D7 Lu Eboldsans 1D5D8 Lu Fboldsans 1D5D9 Lu Gboldsans 1D5DA Lu Hboldsans 1D5DB Lu Iboldsans 1D5DC Lu Jboldsans 1D5DD Lu Kboldsans 1D5DE Lu Lboldsans 1D5DF Lu Mboldsans 1D5E0 Lu Nboldsans 1D5E1 Lu Oboldsans 1D5E2 Lu Pboldsans 1D5E3 Lu Qboldsans 1D5E4 Lu Rboldsans 1D5E5 Lu Sboldsans 1D5E6 Lu Tboldsans 1D5E7 Lu Uboldsans 1D5E8 Lu Vboldsans 1D5E9 Lu Wboldsans 1D5EA Lu Xboldsans 1D5EB Lu Yboldsans 1D5EC Lu Zboldsans 1D5ED Lu aboldsans 1D5EE Ll bboldsans 1D5EF Ll cboldsans 1D5F0 Ll dboldsans 1D5F1 Ll eboldsans 1D5F2 Ll fboldsans 1D5F3 Ll gboldsans 1D5F4 Ll hboldsans 1D5F5 Ll iboldsans 1D5F6 Ll jboldsans 1D5F7 Ll kboldsans 1D5F8 Ll lboldsans 1D5F9 Ll mboldsans 1D5FA Ll nboldsans 1D5FB Ll oboldsans 1D5FC Ll pboldsans 1D5FD Ll qboldsans 1D5FE Ll rboldsans 1D5FF Ll sboldsans 1D600 Ll tboldsans 1D601 Ll uboldsans 1D602 Ll vboldsans 1D603 Ll wboldsans 1D604 Ll xboldsans 1D605 Ll yboldsans 1D606 Ll zboldsans 1D607 Ll Aitalicsans 1D608 Lu Bitalicsans 1D609 Lu Citalicsans 1D60A Lu Ditalicsans 1D60B Lu Eitalicsans 1D60C Lu Fitalicsans 1D60D Lu Gitalicsans 1D60E Lu Hitalicsans 1D60F Lu Iitalicsans 1D610 Lu Jitalicsans 1D611 Lu Kitalicsans 1D612 Lu Litalicsans 1D613 Lu Mitalicsans 1D614 Lu Nitalicsans 1D615 Lu Oitalicsans 1D616 Lu Pitalicsans 1D617 Lu Qitalicsans 1D618 Lu Ritalicsans 1D619 Lu Sitalicsans 1D61A Lu Titalicsans 1D61B Lu Uitalicsans 1D61C Lu Vitalicsans 1D61D Lu Witalicsans 1D61E Lu Xitalicsans 1D61F Lu Yitalicsans 1D620 Lu Zitalicsans 1D621 Lu aitalicsans 1D622 Ll bitalicsans 1D623 Ll citalicsans 1D624 Ll ditalicsans 1D625 Ll eitalicsans 1D626 Ll fitalicsans 1D627 Ll gitalicsans 1D628 Ll hitalicsans 1D629 Ll iitalicsans 1D62A Ll jitalicsans 1D62B Ll kitalicsans 1D62C Ll litalicsans 1D62D Ll mitalicsans 1D62E Ll nitalicsans 1D62F Ll oitalicsans 1D630 Ll pitalicsans 1D631 Ll qitalicsans 1D632 Ll ritalicsans 1D633 Ll sitalicsans 1D634 Ll titalicsans 1D635 Ll uitalicsans 1D636 Ll vitalicsans 1D637 Ll witalicsans 1D638 Ll xitalicsans 1D639 Ll yitalicsans 1D63A Ll zitalicsans 1D63B Ll Abolditalicsans 1D63C Lu Bbolditalicsans 1D63D Lu Cbolditalicsans 1D63E Lu Dbolditalicsans 1D63F Lu Ebolditalicsans 1D640 Lu Fbolditalicsans 1D641 Lu Gbolditalicsans 1D642 Lu Hbolditalicsans 1D643 Lu Ibolditalicsans 1D644 Lu Jbolditalicsans 1D645 Lu Kbolditalicsans 1D646 Lu Lbolditalicsans 1D647 Lu Mbolditalicsans 1D648 Lu Nbolditalicsans 1D649 Lu Obolditalicsans 1D64A Lu Pbolditalicsans 1D64B Lu Qbolditalicsans 1D64C Lu Rbolditalicsans 1D64D Lu Sbolditalicsans 1D64E Lu Tbolditalicsans 1D64F Lu Ubolditalicsans 1D650 Lu Vbolditalicsans 1D651 Lu Wbolditalicsans 1D652 Lu Xbolditalicsans 1D653 Lu Ybolditalicsans 1D654 Lu Zbolditalicsans 1D655 Lu abolditalicsans 1D656 Ll bbolditalicsans 1D657 Ll cbolditalicsans 1D658 Ll dbolditalicsans 1D659 Ll ebolditalicsans 1D65A Ll fbolditalicsans 1D65B Ll gbolditalicsans 1D65C Ll hbolditalicsans 1D65D Ll ibolditalicsans 1D65E Ll jbolditalicsans 1D65F Ll kbolditalicsans 1D660 Ll lbolditalicsans 1D661 Ll mbolditalicsans 1D662 Ll nbolditalicsans 1D663 Ll obolditalicsans 1D664 Ll pbolditalicsans 1D665 Ll qbolditalicsans 1D666 Ll rbolditalicsans 1D667 Ll sbolditalicsans 1D668 Ll tbolditalicsans 1D669 Ll ubolditalicsans 1D66A Ll vbolditalicsans 1D66B Ll wbolditalicsans 1D66C Ll xbolditalicsans 1D66D Ll ybolditalicsans 1D66E Ll zbolditalicsans 1D66F Ll Amono 1D670 Lu Bmono 1D671 Lu Cmono 1D672 Lu Dmono 1D673 Lu Emono 1D674 Lu Fmono 1D675 Lu Gmono 1D676 Lu Hmono 1D677 Lu Imono 1D678 Lu Jmono 1D679 Lu Kmono 1D67A Lu Lmono 1D67B Lu Mmono 1D67C Lu Nmono 1D67D Lu Omono 1D67E Lu Pmono 1D67F Lu Qmono 1D680 Lu Rmono 1D681 Lu Smono 1D682 Lu Tmono 1D683 Lu Umono 1D684 Lu Vmono 1D685 Lu Wmono 1D686 Lu Xmono 1D687 Lu Ymono 1D688 Lu Zmono 1D689 Lu amono 1D68A Ll bmono 1D68B Ll cmono 1D68C Ll dmono 1D68D Ll emono 1D68E Ll fmono 1D68F Ll gmono 1D690 Ll hmono 1D691 Ll imono 1D692 Ll jmono 1D693 Ll kmono 1D694 Ll lmono 1D695 Ll mmono 1D696 Ll nmono 1D697 Ll omono 1D698 Ll pmono 1D699 Ll qmono 1D69A Ll rmono 1D69B Ll smono 1D69C Ll tmono 1D69D Ll umono 1D69E Ll vmono 1D69F Ll wmono 1D6A0 Ll xmono 1D6A1 Ll ymono 1D6A2 Ll zmono 1D6A3 Ll dotlessiitalic 1D6A4 Ll dotlessjitalic 1D6A5 Ll Alphabold 1D6A8 Lu Betabold 1D6A9 Lu Gammabold 1D6AA Lu Deltabold 1D6AB Lu Epsilonbold 1D6AC Lu Zetabold 1D6AD Lu Etabold 1D6AE Lu Thetabold 1D6AF Lu Iotabold 1D6B0 Lu Kappabold 1D6B1 Lu Lamdabold 1D6B2 Lu Mubold 1D6B3 Lu Nubold 1D6B4 Lu Xibold 1D6B5 Lu Omicronbold 1D6B6 Lu Pibold 1D6B7 Lu Rhobold 1D6B8 Lu Thetasymbolbold 1D6B9 Lu Sigmabold 1D6BA Lu Taubold 1D6BB Lu Upsilonbold 1D6BC Lu Phibold 1D6BD Lu Chibold 1D6BE Lu Psibold 1D6BF Lu Omegabold 1D6C0 Lu boldnabla 1D6C1 Sm alphabold 1D6C2 Ll betabold 1D6C3 Ll gammabold 1D6C4 Ll deltabold 1D6C5 Ll epsilonbold 1D6C6 Ll zetabold 1D6C7 Ll etabold 1D6C8 Ll thetabold 1D6C9 Ll iotabold 1D6CA Ll kappabold 1D6CB Ll lamdabold 1D6CC Ll mubold 1D6CD Ll nubold 1D6CE Ll xibold 1D6CF Ll omicronbold 1D6D0 Ll pibold 1D6D1 Ll rhobold 1D6D2 Ll finalsigmabold 1D6D3 Ll sigmabold 1D6D4 Ll taubold 1D6D5 Ll upsilonbold 1D6D6 Ll phibold 1D6D7 Ll chibold 1D6D8 Ll psibold 1D6D9 Ll omegabold 1D6DA Ll boldpartialdiff 1D6DB Sm epsilonsymbolbold 1D6DC Ll thetasymbolbold 1D6DD Ll kappasymbolbold 1D6DE Ll phisymbolbold 1D6DF Ll rhosymbolbold 1D6E0 Ll pisymbolbold 1D6E1 Ll Alphaitalic 1D6E2 Lu Betaitalic 1D6E3 Lu Gammaitalic 1D6E4 Lu Deltaitalic 1D6E5 Lu Epsilonitalic 1D6E6 Lu Zetaitalic 1D6E7 Lu Etaitalic 1D6E8 Lu Thetaitalic 1D6E9 Lu Iotaitalic 1D6EA Lu Kappaitalic 1D6EB Lu Lamdaitalic 1D6EC Lu Muitalic 1D6ED Lu Nuitalic 1D6EE Lu Xiitalic 1D6EF Lu Omicronitalic 1D6F0 Lu Piitalic 1D6F1 Lu Rhoitalic 1D6F2 Lu Thetasymbolitalic 1D6F3 Lu Sigmaitalic 1D6F4 Lu Tauitalic 1D6F5 Lu Upsilonitalic 1D6F6 Lu Phiitalic 1D6F7 Lu Chiitalic 1D6F8 Lu Psiitalic 1D6F9 Lu Omegaitalic 1D6FA Lu italicnabla 1D6FB Sm alphaitalic 1D6FC Ll betaitalic 1D6FD Ll gammaitalic 1D6FE Ll deltaitalic 1D6FF Ll epsilonitalic 1D700 Ll zetaitalic 1D701 Ll etaitalic 1D702 Ll thetaitalic 1D703 Ll iotaitalic 1D704 Ll kappaitalic 1D705 Ll lamdaitalic 1D706 Ll muitalic 1D707 Ll nuitalic 1D708 Ll xiitalic 1D709 Ll omicronitalic 1D70A Ll piitalic 1D70B Ll rhoitalic 1D70C Ll finalsigmaitalic 1D70D Ll sigmaitalic 1D70E Ll tauitalic 1D70F Ll upsilonitalic 1D710 Ll phiitalic 1D711 Ll chiitalic 1D712 Ll psiitalic 1D713 Ll omegaitalic 1D714 Ll italicpartialdiff 1D715 Sm epsilonsymbolitalic 1D716 Ll thetasymbolitalic 1D717 Ll kappasymbolitalic 1D718 Ll phisymbolitalic 1D719 Ll rhosymbolitalic 1D71A Ll pisymbolitalic 1D71B Ll Alphabolditalic 1D71C Lu Betabolditalic 1D71D Lu Gammabolditalic 1D71E Lu Deltabolditalic 1D71F Lu Epsilonbolditalic 1D720 Lu Zetabolditalic 1D721 Lu Etabolditalic 1D722 Lu Thetabolditalic 1D723 Lu Iotabolditalic 1D724 Lu Kappabolditalic 1D725 Lu Lamdabolditalic 1D726 Lu Mubolditalic 1D727 Lu Nubolditalic 1D728 Lu Xibolditalic 1D729 Lu Omicronbolditalic 1D72A Lu Pibolditalic 1D72B Lu Rhobolditalic 1D72C Lu Thetasymbolbolditalic 1D72D Lu Sigmabolditalic 1D72E Lu Taubolditalic 1D72F Lu Upsilonbolditalic 1D730 Lu Phibolditalic 1D731 Lu Chibolditalic 1D732 Lu Psibolditalic 1D733 Lu Omegabolditalic 1D734 Lu bolditalicnabla 1D735 Sm alphabolditalic 1D736 Ll betabolditalic 1D737 Ll gammabolditalic 1D738 Ll deltabolditalic 1D739 Ll epsilonbolditalic 1D73A Ll zetabolditalic 1D73B Ll etabolditalic 1D73C Ll thetabolditalic 1D73D Ll iotabolditalic 1D73E Ll kappabolditalic 1D73F Ll lamdabolditalic 1D740 Ll mubolditalic 1D741 Ll nubolditalic 1D742 Ll xibolditalic 1D743 Ll omicronbolditalic 1D744 Ll pibolditalic 1D745 Ll rhobolditalic 1D746 Ll finalsigmabolditalic 1D747 Ll sigmabolditalic 1D748 Ll taubolditalic 1D749 Ll upsilonbolditalic 1D74A Ll phibolditalic 1D74B Ll chibolditalic 1D74C Ll psibolditalic 1D74D Ll omegabolditalic 1D74E Ll bolditalicpartialdiff 1D74F Sm epsilonsymbolbolditalic 1D750 Ll thetasymbolbolditalic 1D751 Ll kappasymbolbolditalic 1D752 Ll phisymbolbolditalic 1D753 Ll rhosymbolbolditalic 1D754 Ll pisymbolbolditalic 1D755 Ll Alphaboldsans 1D756 Lu Betaboldsans 1D757 Lu Gammaboldsans 1D758 Lu Deltaboldsans 1D759 Lu Epsilonboldsans 1D75A Lu Zetaboldsans 1D75B Lu Etaboldsans 1D75C Lu Thetaboldsans 1D75D Lu Iotaboldsans 1D75E Lu Kappaboldsans 1D75F Lu Lamdaboldsans 1D760 Lu Muboldsans 1D761 Lu Nuboldsans 1D762 Lu Xiboldsans 1D763 Lu Omicronboldsans 1D764 Lu Piboldsans 1D765 Lu Rhoboldsans 1D766 Lu Thetasymbolboldsans 1D767 Lu Sigmaboldsans 1D768 Lu Tauboldsans 1D769 Lu Upsilonboldsans 1D76A Lu Phiboldsans 1D76B Lu Chiboldsans 1D76C Lu Psiboldsans 1D76D Lu Omegaboldsans 1D76E Lu boldsansnabla 1D76F Sm alphaboldsans 1D770 Ll betaboldsans 1D771 Ll gammaboldsans 1D772 Ll deltaboldsans 1D773 Ll epsilonboldsans 1D774 Ll zetaboldsans 1D775 Ll etaboldsans 1D776 Ll thetaboldsans 1D777 Ll iotaboldsans 1D778 Ll kappaboldsans 1D779 Ll lamdaboldsans 1D77A Ll muboldsans 1D77B Ll nuboldsans 1D77C Ll xiboldsans 1D77D Ll omicronboldsans 1D77E Ll piboldsans 1D77F Ll rhoboldsans 1D780 Ll finalsigmaboldsans 1D781 Ll sigmaboldsans 1D782 Ll tauboldsans 1D783 Ll upsilonboldsans 1D784 Ll phiboldsans 1D785 Ll chiboldsans 1D786 Ll psiboldsans 1D787 Ll omegaboldsans 1D788 Ll boldsanspartialdiff 1D789 Sm epsilonsymbolboldsans 1D78A Ll thetasymbolboldsans 1D78B Ll kappasymbolboldsans 1D78C Ll phisymbolboldsans 1D78D Ll rhosymbolboldsans 1D78E Ll pisymbolboldsans 1D78F Ll Alphabolditalicsans 1D790 Lu Betabolditalicsans 1D791 Lu Gammabolditalicsans 1D792 Lu Deltabolditalicsans 1D793 Lu Epsilonbolditalicsans 1D794 Lu Zetabolditalicsans 1D795 Lu Etabolditalicsans 1D796 Lu Thetabolditalicsans 1D797 Lu Iotabolditalicsans 1D798 Lu Kappabolditalicsans 1D799 Lu Lamdabolditalicsans 1D79A Lu Mubolditalicsans 1D79B Lu Nubolditalicsans 1D79C Lu Xibolditalicsans 1D79D Lu Omicronbolditalicsans 1D79E Lu Pibolditalicsans 1D79F Lu Rhobolditalicsans 1D7A0 Lu Thetasymbolbolditalicsans 1D7A1 Lu Sigmabolditalicsans 1D7A2 Lu Taubolditalicsans 1D7A3 Lu Upsilonbolditalicsans 1D7A4 Lu Phibolditalicsans 1D7A5 Lu Chibolditalicsans 1D7A6 Lu Psibolditalicsans 1D7A7 Lu Omegabolditalicsans 1D7A8 Lu bolditalicsansnabla 1D7A9 Sm alphabolditalicsans 1D7AA Ll betabolditalicsans 1D7AB Ll gammabolditalicsans 1D7AC Ll deltabolditalicsans 1D7AD Ll epsilonbolditalicsans 1D7AE Ll zetabolditalicsans 1D7AF Ll etabolditalicsans 1D7B0 Ll thetabolditalicsans 1D7B1 Ll iotabolditalicsans 1D7B2 Ll kappabolditalicsans 1D7B3 Ll lamdabolditalicsans 1D7B4 Ll mubolditalicsans 1D7B5 Ll nubolditalicsans 1D7B6 Ll xibolditalicsans 1D7B7 Ll omicronbolditalicsans 1D7B8 Ll pibolditalicsans 1D7B9 Ll rhobolditalicsans 1D7BA Ll finalsigmabolditalicsans 1D7BB Ll sigmabolditalicsans 1D7BC Ll taubolditalicsans 1D7BD Ll upsilonbolditalicsans 1D7BE Ll phibolditalicsans 1D7BF Ll chibolditalicsans 1D7C0 Ll psibolditalicsans 1D7C1 Ll omegabolditalicsans 1D7C2 Ll bolditalicsanspartialdiff 1D7C3 Sm epsilonsymbolbolditalicsans 1D7C4 Ll thetasymbolbolditalicsans 1D7C5 Ll kappasymbolbolditalicsans 1D7C6 Ll phisymbolbolditalicsans 1D7C7 Ll rhosymbolbolditalicsans 1D7C8 Ll pisymbolbolditalicsans 1D7C9 Ll Digammabold 1D7CA Lu digammabold 1D7CB Ll zerobold 1D7CE Nd onebold 1D7CF Nd twobold 1D7D0 Nd threebold 1D7D1 Nd fourbold 1D7D2 Nd fivebold 1D7D3 Nd sixbold 1D7D4 Nd sevenbold 1D7D5 Nd eightbold 1D7D6 Nd ninebold 1D7D7 Nd zerodblstruck 1D7D8 Nd onedblstruck 1D7D9 Nd twodblstruck 1D7DA Nd threedblstruck 1D7DB Nd fourdblstruck 1D7DC Nd fivedblstruck 1D7DD Nd sixdblstruck 1D7DE Nd sevendblstruck 1D7DF Nd eightdblstruck 1D7E0 Nd ninedblstruck 1D7E1 Nd zerosans 1D7E2 Nd onesans 1D7E3 Nd twosans 1D7E4 Nd threesans 1D7E5 Nd foursans 1D7E6 Nd fivesans 1D7E7 Nd sixsans 1D7E8 Nd sevensans 1D7E9 Nd eightsans 1D7EA Nd ninesans 1D7EB Nd zeroboldsans 1D7EC Nd oneboldsans 1D7ED Nd twoboldsans 1D7EE Nd threeboldsans 1D7EF Nd fourboldsans 1D7F0 Nd fiveboldsans 1D7F1 Nd sixboldsans 1D7F2 Nd sevenboldsans 1D7F3 Nd eightboldsans 1D7F4 Nd nineboldsans 1D7F5 Nd zeromono 1D7F6 Nd onemono 1D7F7 Nd twomono 1D7F8 Nd threemono 1D7F9 Nd fourmono 1D7FA Nd fivemono 1D7FB Nd sixmono 1D7FC Nd sevenmono 1D7FD Nd eightmono 1D7FE Nd ninemono 1D7FF Nd # Domino Tiles dominohorizontalback 1F030 So dominohorizontal_00_00 1F031 So dominohorizontal_00_01 1F032 So dominohorizontal_00_02 1F033 So dominohorizontal_00_03 1F034 So dominohorizontal_00_04 1F035 So dominohorizontal_00_05 1F036 So dominohorizontal_00_06 1F037 So dominohorizontal_01_00 1F038 So dominohorizontal_01_01 1F039 So dominohorizontal_01_02 1F03A So dominohorizontal_01_03 1F03B So dominohorizontal_01_04 1F03C So dominohorizontal_01_05 1F03D So dominohorizontal_01_06 1F03E So dominohorizontal_02_00 1F03F So dominohorizontal_02_01 1F040 So dominohorizontal_02_02 1F041 So dominohorizontal_02_03 1F042 So dominohorizontal_02_04 1F043 So dominohorizontal_02_05 1F044 So dominohorizontal_02_06 1F045 So dominohorizontal_03_00 1F046 So dominohorizontal_03_01 1F047 So dominohorizontal_03_02 1F048 So dominohorizontal_03_03 1F049 So dominohorizontal_03_04 1F04A So dominohorizontal_03_05 1F04B So dominohorizontal_03_06 1F04C So dominohorizontal_04_00 1F04D So dominohorizontal_04_01 1F04E So dominohorizontal_04_02 1F04F So dominohorizontal_04_03 1F050 So dominohorizontal_04_04 1F051 So dominohorizontal_04_05 1F052 So dominohorizontal_04_06 1F053 So dominohorizontal_05_00 1F054 So dominohorizontal_05_01 1F055 So dominohorizontal_05_02 1F056 So dominohorizontal_05_03 1F057 So dominohorizontal_05_04 1F058 So dominohorizontal_05_05 1F059 So dominohorizontal_05_06 1F05A So dominohorizontal_06_00 1F05B So dominohorizontal_06_01 1F05C So dominohorizontal_06_02 1F05D So dominohorizontal_06_03 1F05E So dominohorizontal_06_04 1F05F So dominohorizontal_06_05 1F060 So dominohorizontal_06_06 1F061 So dominoverticalback 1F062 So dominovertical_00_00 1F063 So dominovertical_00_01 1F064 So dominovertical_00_02 1F065 So dominovertical_00_03 1F066 So dominovertical_00_04 1F067 So dominovertical_00_05 1F068 So dominovertical_00_06 1F069 So dominovertical_01_00 1F06A So dominovertical_01_01 1F06B So dominovertical_01_02 1F06C So dominovertical_01_03 1F06D So dominovertical_01_04 1F06E So dominovertical_01_05 1F06F So dominovertical_01_06 1F070 So dominovertical_02_00 1F071 So dominovertical_02_01 1F072 So dominovertical_02_02 1F073 So dominovertical_02_03 1F074 So dominovertical_02_04 1F075 So dominovertical_02_05 1F076 So dominovertical_02_06 1F077 So dominovertical_03_00 1F078 So dominovertical_03_01 1F079 So dominovertical_03_02 1F07A So dominovertical_03_03 1F07B So dominovertical_03_04 1F07C So dominovertical_03_05 1F07D So dominovertical_03_06 1F07E So dominovertical_04_00 1F07F So dominovertical_04_01 1F080 So dominovertical_04_02 1F081 So dominovertical_04_03 1F082 So dominovertical_04_04 1F083 So dominovertical_04_05 1F084 So dominovertical_04_06 1F085 So dominovertical_05_00 1F086 So dominovertical_05_01 1F087 So dominovertical_05_02 1F088 So dominovertical_05_03 1F089 So dominovertical_05_04 1F08A So dominovertical_05_05 1F08B So dominovertical_05_06 1F08C So dominovertical_06_00 1F08D So dominovertical_06_01 1F08E So dominovertical_06_02 1F08F So dominovertical_06_03 1F090 So dominovertical_06_04 1F091 So dominovertical_06_05 1F092 So dominovertical_06_06 1F093 So # Playing Cards cards:back 1F0A0 So cards:aceofspades 1F0A1 So cards:twoofspades 1F0A2 So cards:threeofspades 1F0A3 So cards:fourofspades 1F0A4 So cards:fiveofspades 1F0A5 So cards:sixofspades 1F0A6 So cards:sevenofspades 1F0A7 So cards:eightofspades 1F0A8 So cards:nineofspades 1F0A9 So cards:tenofspades 1F0AA So cards:jackofspades 1F0AB So cards:knightofspades 1F0AC So cards:queenofspades 1F0AD So cards:kingofspades 1F0AE So cards:aceofhearts 1F0B1 So cards:twoofhearts 1F0B2 So cards:threeofhearts 1F0B3 So cards:fourofhearts 1F0B4 So cards:fiveofhearts 1F0B5 So cards:sixofhearts 1F0B6 So cards:sevenofhearts 1F0B7 So cards:eightofhearts 1F0B8 So cards:nineofhearts 1F0B9 So cards:tenofhearts 1F0BA So cards:jackofhearts 1F0BB So cards:knightofhearts 1F0BC So cards:queenofhearts 1F0BD So cards:kingofhearts 1F0BE So cards:redjoker 1F0BF So cards:aceofdiamonds 1F0C1 So cards:twoofdiamonds 1F0C2 So cards:threeofdiamonds 1F0C3 So cards:fourofdiamonds 1F0C4 So cards:fiveofdiamonds 1F0C5 So cards:sixofdiamonds 1F0C6 So cards:sevenofdiamonds 1F0C7 So cards:eightofdiamonds 1F0C8 So cards:nineofdiamonds 1F0C9 So cards:tenofdiamonds 1F0CA So cards:jackofdiamonds 1F0CB So cards:knightofdiamonds 1F0CC So cards:queenofdiamonds 1F0CD So cards:kingofdiamonds 1F0CE So cards:blackjoker 1F0CF So cards:aceofclubs 1F0D1 So cards:twoofclubs 1F0D2 So cards:threeofclubs 1F0D3 So cards:fourofclubs 1F0D4 So cards:fiveofclubs 1F0D5 So cards:sixofclubs 1F0D6 So cards:sevenofclubs 1F0D7 So cards:eightofclubs 1F0D8 So cards:nineofclubs 1F0D9 So cards:tenofclubs 1F0DA So cards:jackofclubs 1F0DB So cards:knightofclubs 1F0DC So cards:queenofclubs 1F0DD So cards:kingofclubs 1F0DE So cards:whitejoker 1F0DF So cards:fool 1F0E0 So cards:trump1 1F0E1 So cards:trump2 1F0E2 So cards:trump3 1F0E3 So cards:trump4 1F0E4 So cards:trump5 1F0E5 So cards:trump6 1F0E6 So cards:trump7 1F0E7 So cards:trump8 1F0E8 So cards:trump9 1F0E9 So cards:trump10 1F0EA So cards:trump11 1F0EB So cards:trump12 1F0EC So cards:trump13 1F0ED So cards:trump14 1F0EE So cards:trump15 1F0EF So cards:trump16 1F0F0 So cards:trump17 1F0F1 So cards:trump18 1F0F2 So cards:trump19 1F0F3 So cards:trump20 1F0F4 So cards:trump21 1F0F5 So # Enclosed Alphanumeric Supplement zerofullstop 1F100 No zerocomma 1F101 No onecomma 1F102 No twocomma 1F103 No threecomma 1F104 No fourcomma 1F105 No fivecomma 1F106 No sixcomma 1F107 No sevencomma 1F108 No eightcomma 1F109 No ninecomma 1F10A No dingbatSAns-serifzerocircle 1F10B No dingbatSAns-serifzerocircleblack 1F10C No Aparens 1F110 So Bparens 1F111 So Cparens 1F112 So Dparens 1F113 So Eparens 1F114 So Fparens 1F115 So Gparens 1F116 So Hparens 1F117 So Iparens 1F118 So Jparens 1F119 So Kparens 1F11A So Lparens 1F11B So Mparens 1F11C So Nparens 1F11D So Oparens 1F11E So Pparens 1F11F So Qparens 1F120 So Rparens 1F121 So Sparens 1F122 So Tparens 1F123 So Uparens 1F124 So Vparens 1F125 So Wparens 1F126 So Xparens 1F127 So Yparens 1F128 So Zparens 1F129 So Sshell 1F12A So Citaliccircle 1F12B So Ritaliccircle 1F12C So CDcircle 1F12D So WZcircle 1F12E So copyleftsymbol 1F12F So Asquare 1F130 So Bsquare 1F131 So Csquare 1F132 So Dsquare 1F133 So Esquare 1F134 So Fsquare 1F135 So Gsquare 1F136 So Hsquare 1F137 So Isquare 1F138 So Jsquare 1F139 So Ksquare 1F13A So Lsquare 1F13B So Msquare 1F13C So Nsquare 1F13D So Osquare 1F13E So Psquare 1F13F So Qsquare 1F140 So Rsquare 1F141 So Ssquare 1F142 So Tsquare 1F143 So Usquare 1F144 So Vsquare 1F145 So Wsquare 1F146 So Xsquare 1F147 So Ysquare 1F148 So Zsquare 1F149 So HVsquare 1F14A So MVsquare 1F14B So SDsquare 1F14C So SSsquare 1F14D So PPVsquare 1F14E So wcsquare 1F14F So Acircleblack 1F150 So Bcircleblack 1F151 So Ccircleblack 1F152 So Dcircleblack 1F153 So Ecircleblack 1F154 So Fcircleblack 1F155 So Gcircleblack 1F156 So Hcircleblack 1F157 So Icircleblack 1F158 So Jcircleblack 1F159 So Kcircleblack 1F15A So Lcircleblack 1F15B So Mcircleblack 1F15C So Ncircleblack 1F15D So Ocircleblack 1F15E So Pcircleblack 1F15F So Qcircleblack 1F160 So Rcircleblack 1F161 So Scircleblack 1F162 So Tcircleblack 1F163 So Ucircleblack 1F164 So Vcircleblack 1F165 So Wcircleblack 1F166 So Xcircleblack 1F167 So Ycircleblack 1F168 So Zcircleblack 1F169 So raisedmcsign 1F16A So raisedmdsign 1F16B So raisedmrsign 1F16C So Asquareblack 1F170 So Bsquareblack 1F171 So Csquareblack 1F172 So Dsquareblack 1F173 So Esquareblack 1F174 So Fsquareblack 1F175 So Gsquareblack 1F176 So Hsquareblack 1F177 So Isquareblack 1F178 So Jsquareblack 1F179 So Ksquareblack 1F17A So Lsquareblack 1F17B So Msquareblack 1F17C So Nsquareblack 1F17D So Osquareblack 1F17E So Psquareblack 1F17F So Qsquareblack 1F180 So Rsquareblack 1F181 So Ssquareblack 1F182 So Tsquareblack 1F183 So Usquareblack 1F184 So Vsquareblack 1F185 So Wsquareblack 1F186 So Xsquareblack 1F187 So Ysquareblack 1F188 So Zsquareblack 1F189 So Pcrosssquareblack 1F18A So ICsquareblack 1F18B So PAsquareblack 1F18C So SAsquareblack 1F18D So absquareblack 1F18E So wcsquareblack 1F18F So squaredj 1F190 So clsquare 1F191 So coolsquare 1F192 So freesquare 1F193 So idsquare 1F194 So newsquare 1F195 So ngsquare 1F196 So oksquare 1F197 So sossquare 1F198 So upwithexclamationmarksquare 1F199 So vssquare 1F19A So threedsquare 1F19B So secondscreensquare 1F19C So twoksquare 1F19D So fourksquare 1F19E So eightksquare 1F19F So fivepointonesquare 1F1A0 So sevenpointonesquare 1F1A1 So twenty-twopointtwosquare 1F1A2 So sixtypsquare 1F1A3 So onehundredtwentypsquare 1F1A4 So dsquare 1F1A5 So hcsquare 1F1A6 So hdrsquare 1F1A7 So hi-ressquare 1F1A8 So losslesssquare 1F1A9 So shvsquare 1F1AA So uhdsquare 1F1AB So vodsquare 1F1AC So regionalindicatorsymbollettera 1F1E6 So regionalindicatorsymbolletterb 1F1E7 So regionalindicatorsymbolletterc 1F1E8 So regionalindicatorsymbolletterd 1F1E9 So regionalindicatorsymbollettere 1F1EA So regionalindicatorsymbolletterf 1F1EB So regionalindicatorsymbolletterg 1F1EC So regionalindicatorsymbolletterh 1F1ED So regionalindicatorsymbolletteri 1F1EE So regionalindicatorsymbolletterj 1F1EF So regionalindicatorsymbolletterk 1F1F0 So regionalindicatorsymbolletterl 1F1F1 So regionalindicatorsymbolletterm 1F1F2 So regionalindicatorsymbollettern 1F1F3 So regionalindicatorsymbollettero 1F1F4 So regionalindicatorsymbolletterp 1F1F5 So regionalindicatorsymbolletterq 1F1F6 So regionalindicatorsymbolletterr 1F1F7 So regionalindicatorsymbolletters 1F1F8 So regionalindicatorsymbollettert 1F1F9 So regionalindicatorsymbolletteru 1F1FA So regionalindicatorsymbolletterv 1F1FB So regionalindicatorsymbolletterw 1F1FC So regionalindicatorsymbolletterx 1F1FD So regionalindicatorsymbollettery 1F1FE So regionalindicatorsymbolletterz 1F1FF So # Miscellaneous Symbols and Pictographs cyclone 1F300 So foggy 1F301 So closedUmbrella 1F302 So nightStars 1F303 So sunriseOverMountains 1F304 So sunrise 1F305 So cityscapeAtDusk 1F306 So sunsetOverBuildings 1F307 So rainbow 1F308 So bridgeAtNight 1F309 So waterWave 1F30A So volcano 1F30B So milkyWay 1F30C So earthGlobeEuropeAfrica 1F30D So earthGlobeAmericas 1F30E So earthGlobeAsiaAustralia 1F30F So globeMeridians 1F310 So newMoon 1F311 So waxingCrescentMoon 1F312 So firstQuarterMoon 1F313 So waxingGibbousMoon 1F314 So fullMoon 1F315 So waningGibbousMoon 1F316 So lastQuarterMoon 1F317 So waningCrescentMoon 1F318 So crescentMoon 1F319 So newMoonFace 1F31A So firstQuarterMoonFace 1F31B So lastQuarterMoonFace 1F31C So fullMoonFace 1F31D So sunFace 1F31E So glowingStar 1F31F So shootingStar 1F320 So thermometer 1F321 So blackDroplet 1F322 So whiteSun 1F323 So whiteSunSmallCloud 1F324 So whiteSunBehindCloud 1F325 So whiteSunBehindCloudRain 1F326 So cloudRain 1F327 So cloudSnow 1F328 So cloudLightning 1F329 So cloudTornado 1F32A So fog 1F32B So windBlowingFace 1F32C So hotDog 1F32D So taco 1F32E So burrito 1F32F So chestnut 1F330 So seedling 1F331 So evergreenTree 1F332 So deciduousTree 1F333 So palmTree 1F334 So cactus 1F335 So hotPepper 1F336 So tulip 1F337 So cherryBlossom 1F338 So rose 1F339 So hibiscus 1F33A So sunflower 1F33B So blossom 1F33C So earOfMaize 1F33D So earOfRice 1F33E So herb 1F33F So fourLeafClover 1F340 So mapleLeaf 1F341 So fallenLeaf 1F342 So leafFlutteringInWind 1F343 So mushroom 1F344 So tomato 1F345 So aubergine 1F346 So grapes 1F347 So melon 1F348 So watermelon 1F349 So tangerine 1F34A So lemon 1F34B So banana 1F34C So pineapple 1F34D So redApple 1F34E So greenApple 1F34F So pear 1F350 So peach 1F351 So cherries 1F352 So strawberry 1F353 So hamburger 1F354 So sliceOfPizza 1F355 So meatOnBone 1F356 So poultryLeg 1F357 So riceCracker 1F358 So riceBall 1F359 So cookedRice 1F35A So curryAndRice 1F35B So steamingBowl 1F35C So spaghetti 1F35D So bread 1F35E So frenchFries 1F35F So roastedSweetPotato 1F360 So dango 1F361 So oden 1F362 So sushi 1F363 So friedShrimp 1F364 So fishCakeSwirlDesign 1F365 So softIceCream 1F366 So shavedIce 1F367 So iceCream 1F368 So doughnut 1F369 So cookie 1F36A So chocolateBar 1F36B So candy 1F36C So lollipop 1F36D So custard 1F36E So honeyPot 1F36F So shortcake 1F370 So bentoBox 1F371 So potOfFood 1F372 So cooking 1F373 So forkKnife 1F374 So teacupOutHandle 1F375 So sakeBottleAndCup 1F376 So wineGlass 1F377 So cocktailGlass 1F378 So tropicalDrink 1F379 So beerMug 1F37A So clinkingBeerMugs 1F37B So babyBottle 1F37C So forkKnifePlate 1F37D So bottlePoppingCork 1F37E So popcorn 1F37F So ribbon 1F380 So wrappedPresent 1F381 So birthdayCake 1F382 So jackOLantern 1F383 So christmasTree 1F384 So fatherChristmas 1F385 So fireworks 1F386 So fireworkSparkler 1F387 So balloon 1F388 So partyPopper 1F389 So confettiBall 1F38A So tanabataTree 1F38B So crossedFlags 1F38C So pineDecoration 1F38D So japaneseDolls 1F38E So carpStreamer 1F38F So windChime 1F390 So moonViewingCeremony 1F391 So schoolSatchel 1F392 So graduationCap 1F393 So heartTipOnTheLeft 1F394 So bouquetOfFlowers 1F395 So militaryMedal 1F396 So reminderRibbon 1F397 So musicalKeyboardJacks 1F398 So studioMicrophone 1F399 So levelSlider 1F39A So controlKnobs 1F39B So beamedAscendingMusicalNotes 1F39C So beamedDescendingMusicalNotes 1F39D So filmFrames 1F39E So admissionTickets 1F39F So carouselHorse 1F3A0 So ferrisWheel 1F3A1 So rollerCoaster 1F3A2 So fishingPoleAndFish 1F3A3 So microphone 1F3A4 So movieCamera 1F3A5 So cinema 1F3A6 So headphone 1F3A7 So artistPalette 1F3A8 So topHat 1F3A9 So circusTent 1F3AA So ticket 1F3AB So clapperBoard 1F3AC So performingArts 1F3AD So videoGame 1F3AE So directHit 1F3AF So slotMachine 1F3B0 So billiards 1F3B1 So gameDie 1F3B2 So bowling 1F3B3 So flowerPlayingCards 1F3B4 So musicalNote 1F3B5 So multipleMusicalNotes 1F3B6 So saxophone 1F3B7 So guitar 1F3B8 So musicalKeyboard 1F3B9 So trumpet 1F3BA So violin 1F3BB So musicalScore 1F3BC So runningShirtSash 1F3BD So tennisRacquetAndBall 1F3BE So skiAndSkiBoot 1F3BF So basketballAndHoop 1F3C0 So chequeredFlag 1F3C1 So snowboarder 1F3C2 So runner 1F3C3 So surfer 1F3C4 So sportsMedal 1F3C5 So trophy 1F3C6 So horseRacing 1F3C7 So americanFootball 1F3C8 So rugbyFootball 1F3C9 So swimmer 1F3CA So weightLifter 1F3CB So golfer 1F3CC So racingMotorcycle 1F3CD So racingCar 1F3CE So cricketBatAndBall 1F3CF So volleyball 1F3D0 So fieldHockeyStickAndBall 1F3D1 So iceHockeyStickAndPuck 1F3D2 So tableTennisPaddleAndBall 1F3D3 So snowcappedMountain 1F3D4 So camping 1F3D5 So beachUmbrella 1F3D6 So buildingConstruction 1F3D7 So houseBuildings 1F3D8 So cityscape 1F3D9 So derelictHouseBuilding 1F3DA So classicalBuilding 1F3DB So desert 1F3DC So desertIsland 1F3DD So nationalPark 1F3DE So stadium 1F3DF So houseBuilding 1F3E0 So houseGarden 1F3E1 So officeBuilding 1F3E2 So japanesePostOffice 1F3E3 So europeanPostOffice 1F3E4 So hospital 1F3E5 So bank 1F3E6 So automatedTellerMachine 1F3E7 So hotel 1F3E8 So loveHotel 1F3E9 So convenienceStore 1F3EA So school 1F3EB So departmentStore 1F3EC So factory 1F3ED So izakayaLantern 1F3EE So japaneseCastle 1F3EF So europeanCastle 1F3F0 So whitePennant 1F3F1 So blackPennant 1F3F2 So wavingWhiteFlag 1F3F3 So wavingBlackFlag 1F3F4 So rosette 1F3F5 So blackRosette 1F3F6 So label 1F3F7 So badmintonRacquetAndShuttlecock 1F3F8 So bowAndArrow 1F3F9 So amphora 1F3FA So emojiModifierFitzpatrickType-1-2 1F3FB Sk emojiModifierFitzpatrickType-3 1F3FC Sk emojiModifierFitzpatrickType-4 1F3FD Sk emojiModifierFitzpatrickType-5 1F3FE Sk emojiModifierFitzpatrickType-6 1F3FF Sk rat 1F400 So mouse 1F401 So ox 1F402 So waterBuffalo 1F403 So cow 1F404 So tiger 1F405 So leopard 1F406 So rabbit 1F407 So cat 1F408 So dragon 1F409 So crocodile 1F40A So whale 1F40B So snail 1F40C So snake 1F40D So horse 1F40E So ram 1F40F So goat 1F410 So sheep 1F411 So monkey 1F412 So rooster 1F413 So chicken 1F414 So dog 1F415 So pig 1F416 So boar 1F417 So elephant 1F418 So octopus 1F419 So spiralShell 1F41A So bug 1F41B So ant 1F41C So honeybee 1F41D So ladyBeetle 1F41E So fish 1F41F So tropicalFish 1F420 So blowfish 1F421 So turtle 1F422 So hatchingChick 1F423 So babyChick 1F424 So front-facingBabyChick 1F425 So bird 1F426 So penguin 1F427 So koala 1F428 So poodle 1F429 So dromedaryCamel 1F42A So bactrianCamel 1F42B So dolphin 1F42C So mouseFace 1F42D So cowFace 1F42E So tigerFace 1F42F So rabbitFace 1F430 So catFace 1F431 So dragonFace 1F432 So spoutingWhale 1F433 So horseFace 1F434 So monkeyFace 1F435 So dogFace 1F436 So pigFace 1F437 So frogFace 1F438 So hamsterFace 1F439 So wolfFace 1F43A So bearFace 1F43B So pandaFace 1F43C So pigNose 1F43D So pawPrints 1F43E So chipmunk 1F43F So eyes 1F440 So eye 1F441 So ear 1F442 So nose 1F443 So mouth 1F444 So tongue 1F445 So whiteUpPointingBackhandIndex 1F446 So whiteDownPointingBackhandIndex 1F447 So whiteLeftPointingBackhandIndex 1F448 So whiteRightPointingBackhandIndex 1F449 So fistedHandSign 1F44A So wavingHandSign 1F44B So okHandSign 1F44C So thumbsUpSign 1F44D So thumbsDownSign 1F44E So clappingHandsSign 1F44F So openHandsSign 1F450 So crown 1F451 So womansHat 1F452 So eyeglasses 1F453 So necktie 1F454 So t-shirt 1F455 So jeans 1F456 So dress 1F457 So kimono 1F458 So bikini 1F459 So womansClothes 1F45A So purse 1F45B So handbag 1F45C So pouch 1F45D So mansShoe 1F45E So athleticShoe 1F45F So high-heeledShoe 1F460 So womansSandal 1F461 So womansBoots 1F462 So footprints 1F463 So bustInSilhouette 1F464 So bustsInSilhouette 1F465 So boy 1F466 So girl 1F467 So man 1F468 So woman 1F469 So family 1F46A So manAndWomanHoldingHands 1F46B So twoMenHoldingHands 1F46C So twoWomenHoldingHands 1F46D So policeOfficer 1F46E So womanBunnyEars 1F46F So brideVeil 1F470 So personBlondHair 1F471 So manGuaPiMao 1F472 So manTurban 1F473 So olderMan 1F474 So olderWoman 1F475 So misc:baby 1F476 So constructionWorker 1F477 So princess 1F478 So japaneseOgre 1F479 So japaneseGoblin 1F47A So ghost 1F47B So babyAngel 1F47C So extraterrestrialAlien 1F47D So alienMonster 1F47E So imp 1F47F So skull 1F480 So inmationDeskPerson 1F481 So guardsman 1F482 So dancer 1F483 So lipstick 1F484 So nailPolish 1F485 So faceMassage 1F486 So haircut 1F487 So barberPole 1F488 So syringe 1F489 So pill 1F48A So kissMark 1F48B So loveLetter 1F48C So misc:ring 1F48D So gemStone 1F48E So kiss 1F48F So bouquet 1F490 So coupleHeart 1F491 So wedding 1F492 So beatingHeart 1F493 So brokenHeart 1F494 So twoHearts 1F495 So sparklingHeart 1F496 So growingHeart 1F497 So heartArrow 1F498 So blueHeart 1F499 So greenHeart 1F49A So yellowHeart 1F49B So purpleHeart 1F49C So heartRibbon 1F49D So revolvingHearts 1F49E So heartDecoration 1F49F So diamondShapeADotInside 1F4A0 So electricLightBulb 1F4A1 So anger 1F4A2 So bomb 1F4A3 So sleeping 1F4A4 So collision 1F4A5 So splashingSweat 1F4A6 So droplet 1F4A7 So misc:dash 1F4A8 So pileOfPoo 1F4A9 So flexedBiceps 1F4AA So dizzy 1F4AB So speechBalloon 1F4AC So thoughtBalloon 1F4AD So whiteFlower 1F4AE So hundredPoints 1F4AF So moneyBag 1F4B0 So currencyExchange 1F4B1 So heavyDollarSign 1F4B2 So creditCard 1F4B3 So banknoteYenSign 1F4B4 So banknoteDollarSign 1F4B5 So banknoteEuroSign 1F4B6 So banknotePoundSign 1F4B7 So moneyWings 1F4B8 So chartUpwardsTrendAndYenSign 1F4B9 So seat 1F4BA So personalComputer 1F4BB So briefcase 1F4BC So minidisc 1F4BD So floppyDisk 1F4BE So opticalDisc 1F4BF So dvd 1F4C0 So fileFolder 1F4C1 So openFileFolder 1F4C2 So pageCurl 1F4C3 So pageFacingUp 1F4C4 So calendar 1F4C5 So tear-offCalendar 1F4C6 So cardIndex 1F4C7 So chartUpwardsTrend 1F4C8 So chartDownwardsTrend 1F4C9 So barChart 1F4CA So clipboard 1F4CB So pushpin 1F4CC So roundPushpin 1F4CD So paperclip 1F4CE So straightRuler 1F4CF So triangularRuler 1F4D0 So bookmarkTabs 1F4D1 So ledger 1F4D2 So notebook 1F4D3 So notebookDecorativeCover 1F4D4 So closedBook 1F4D5 So openBook 1F4D6 So greenBook 1F4D7 So blueBook 1F4D8 So orangeBook 1F4D9 So books 1F4DA So nameBadge 1F4DB So scroll 1F4DC So memo 1F4DD So telephoneReceiver 1F4DE So pager 1F4DF So faxMachine 1F4E0 So satelliteAntenna 1F4E1 So publicAddressLoudspeaker 1F4E2 So cheeringMegaphone 1F4E3 So outboxTray 1F4E4 So inboxTray 1F4E5 So package 1F4E6 So e-mail 1F4E7 So incomingEnvelope 1F4E8 So envelopeDownwardsArrowAbove 1F4E9 So closedMailboxLoweredFlag 1F4EA So closedMailboxRaisedFlag 1F4EB So openMailboxRaisedFlag 1F4EC So openMailboxLoweredFlag 1F4ED So postbox 1F4EE So postalHorn 1F4EF So newspaper 1F4F0 So mobilePhone 1F4F1 So mobilePhoneRightwardsArrowAtLeft 1F4F2 So vibrationMode 1F4F3 So mobilePhoneOff 1F4F4 So noMobilePhones 1F4F5 So antennaBars 1F4F6 So camera 1F4F7 So cameraFlash 1F4F8 So videoCamera 1F4F9 So television 1F4FA So radio 1F4FB So videocassette 1F4FC So filmProjector 1F4FD So portableStereo 1F4FE So prayerBeads 1F4FF So twistedRightwardsArrows 1F500 So clockwiseRightwardsAndLeftwardsOpenCircleArrows 1F501 So clockwiseRightwardsAndLeftwardsOpenCircleArrowsCircledOneOverlay 1F502 So clockwiseDownwardsAndUpwardsOpenCircleArrows 1F503 So anticlockwiseDownwardsAndUpwardsOpenCircleArrows 1F504 So lowBrightness 1F505 So highBrightness 1F506 So speakerCancellationStroke 1F507 So speaker 1F508 So speakerOneSoundWave 1F509 So speakerThreeSoundWaves 1F50A So battery 1F50B So electricPlug 1F50C So left-pointingMagnifyingGlass 1F50D So right-pointingMagnifyingGlass 1F50E So lockInkPen 1F50F So closedLockKey 1F510 So key 1F511 So lock 1F512 So openLock 1F513 So misc:bell 1F514 So bellCancellationStroke 1F515 So bookmark 1F516 So link 1F517 So radioButton 1F518 So backLeftwardsArrowAbove 1F519 So endLeftwardsArrowAbove 1F51A So onExclamationMarkLeftRightArrowAbove 1F51B So soonRightwardsArrowAbove 1F51C So topUpwardsArrowAbove 1F51D So noOneUnderEighteen 1F51E So keycapTen 1F51F So inputLatinCapitalLetters 1F520 So inputLatinSmallLetters 1F521 So inputNumbers 1F522 So inputS 1F523 So inputLatinLetters 1F524 So fire 1F525 So electricTorch 1F526 So wrench 1F527 So hammer 1F528 So nutAndBolt 1F529 So hocho 1F52A So pistol 1F52B So microscope 1F52C So telescope 1F52D So crystalBall 1F52E So sixPointedStarMiddleDot 1F52F So japaneseBeginner 1F530 So tridentEmblem 1F531 So blackSquareButton 1F532 So whiteSquareButton 1F533 So largeRedCircle 1F534 So largeBlueCircle 1F535 So largeOrangeDiamond 1F536 So largeBlueDiamond 1F537 So smallOrangeDiamond 1F538 So smallBlueDiamond 1F539 So redTriangleUp 1F53A So redTriangleDOwn 1F53B So smallRedTriangleUp 1F53C So smallRedTriangleDOwn 1F53D So lowerRightShadowedWhiteCircle 1F53E So upperRightShadowedWhiteCircle 1F53F So circledCrossPommee 1F540 So crossPommeeHalf-circleBelow 1F541 So crossPommee 1F542 So notchedLeftSemicircleThreeDots 1F543 So notchedRightSemicircleThreeDots 1F544 So marksChapter 1F545 So whiteLatinCross 1F546 So heavyLatinCross 1F547 So celticCross 1F548 So om 1F549 So doveOfPeace 1F54A So kaaba 1F54B So mosque 1F54C So synagogue 1F54D So menorahNineBranches 1F54E So bowlOfHygieia 1F54F So clockFaceOneOclock 1F550 So clockFaceTwoOclock 1F551 So clockFaceThreeOclock 1F552 So clockFaceFourOclock 1F553 So clockFaceFiveOclock 1F554 So clockFaceSixOclock 1F555 So clockFaceSevenOclock 1F556 So clockFaceEightOclock 1F557 So clockFaceNineOclock 1F558 So clockFaceTenOclock 1F559 So clockFaceElevenOclock 1F55A So clockFaceTwelveOclock 1F55B So clockFaceOne-thirty 1F55C So clockFaceTwo-thirty 1F55D So clockFaceThree-thirty 1F55E So clockFaceFour-thirty 1F55F So clockFaceFive-thirty 1F560 So clockFaceSix-thirty 1F561 So clockFaceSeven-thirty 1F562 So clockFaceEight-thirty 1F563 So clockFaceNine-thirty 1F564 So clockFaceTen-thirty 1F565 So clockFaceEleven-thirty 1F566 So clockFaceTwelve-thirty 1F567 So rightSpeaker 1F568 So rightSpeakerOneSoundWave 1F569 So rightSpeakerThreeSoundWaves 1F56A So bullhorn 1F56B So bullhornSoundWaves 1F56C So ringingBell 1F56D So book 1F56E So candle 1F56F So mantelpieceClock 1F570 So blackSkullAndCrossbones 1F571 So noPiracy 1F572 So hole 1F573 So manInBusinessSuitLevitating 1F574 So sleuthOrSpy 1F575 So darkSunglasses 1F576 So spider 1F577 So spiderWeb 1F578 So joystick 1F579 So manDancing 1F57A So leftHandTelephoneReceiver 1F57B So telephoneReceiverPage 1F57C So rightHandTelephoneReceiver 1F57D So whiteTouchtoneTelephone 1F57E So blackTouchtoneTelephone 1F57F So telephoneOnTopOfModem 1F580 So clamshellMobilePhone 1F581 So backOfEnvelope 1F582 So stampedEnvelope 1F583 So envelopeLightning 1F584 So flyingEnvelope 1F585 So penOverStampedEnvelope 1F586 So linkedPaperclips 1F587 So blackPushpin 1F588 So lowerLeftPencil 1F589 So lowerLeftBallpointPen 1F58A So lowerLeftFountainPen 1F58B So lowerLeftPaintbrush 1F58C So lowerLeftCrayon 1F58D So leftWritingHand 1F58E So turnedOkHandSign 1F58F So raisedHandFingersSplayed 1F590 So reversedRaisedHandFingersSplayed 1F591 So reversedThumbsUpSign 1F592 So reversedThumbsDownSign 1F593 So reversedVictoryHand 1F594 So reversedHandMiddleFingerExtended 1F595 So raisedHandPartBetweenMiddleAndRingFingers 1F596 So whiteDownPointingLeftHandIndex 1F597 So sidewaysWhiteLeftPointingIndex 1F598 So sidewaysWhiteRightPointingIndex 1F599 So sidewaysBlackLeftPointingIndex 1F59A So sidewaysBlackRightPointingIndex 1F59B So blackLeftPointingBackhandIndex 1F59C So blackRightPointingBackhandIndex 1F59D So sidewaysWhiteUpPointingIndex 1F59E So sidewaysWhiteDownPointingIndex 1F59F So sidewaysBlackUpPointingIndex 1F5A0 So sidewaysBlackDownPointingIndex 1F5A1 So blackUpPointingBackhandIndex 1F5A2 So blackDownPointingBackhandIndex 1F5A3 So blackHeart 1F5A4 So desktopComputer 1F5A5 So keyboardAndMouse 1F5A6 So threeNetworkedComputers 1F5A7 So printer 1F5A8 So pocketCalculator 1F5A9 So blackHardShellFloppyDisk 1F5AA So whiteHardShellFloppyDisk 1F5AB So softShellFloppyDisk 1F5AC So tapeCartridge 1F5AD So wiredKeyboard 1F5AE So oneButtonMouse 1F5AF So twoButtonMouse 1F5B0 So threeButtonMouse 1F5B1 So trackball 1F5B2 So oldPersonalComputer 1F5B3 So hardDisk 1F5B4 So screen 1F5B5 So printerIcon 1F5B6 So faxIcon 1F5B7 So opticalDiscIcon 1F5B8 So documentText 1F5B9 So documentTextAndPicture 1F5BA So documentPicture 1F5BB So framePicture 1F5BC So frameTiles 1F5BD So frameAnX 1F5BE So blackFolder 1F5BF So folder 1F5C0 So openFolder 1F5C1 So cardIndexDividers 1F5C2 So cardFileBox 1F5C3 So fileCabinet 1F5C4 So emptyNote 1F5C5 So emptyNotePage 1F5C6 So emptyNotePad 1F5C7 So note 1F5C8 So notePage 1F5C9 So notePad 1F5CA So emptyDocument 1F5CB So emptyPage 1F5CC So emptyPages 1F5CD So document 1F5CE So page 1F5CF So pages 1F5D0 So wastebasket 1F5D1 So spiralNotePad 1F5D2 So spiralCalendarPad 1F5D3 So desktopWindow 1F5D4 So minimize 1F5D5 So maximize 1F5D6 So overlap 1F5D7 So clockwiseRightAndLeftSemicircleArrows 1F5D8 So cancellationX 1F5D9 So increaseFontSize 1F5DA So decreaseFontSize 1F5DB So compression 1F5DC So oldKey 1F5DD So rolled-upNewspaper 1F5DE So pageCircledText 1F5DF So stockChart 1F5E0 So daggerKnife 1F5E1 So lips 1F5E2 So speakingHeadInSilhouette 1F5E3 So threeRaysAbove 1F5E4 So threeRaysBelow 1F5E5 So threeRaysLeft 1F5E6 So threeRaysRight 1F5E7 So leftSpeechBubble 1F5E8 So rightSpeechBubble 1F5E9 So twoSpeechBubbles 1F5EA So threeSpeechBubbles 1F5EB So leftThoughtBubble 1F5EC So rightThoughtBubble 1F5ED So leftAngerBubble 1F5EE So rightAngerBubble 1F5EF So moodBubble 1F5F0 So lightningMoodBubble 1F5F1 So lightningMood 1F5F2 So ballotBoxBallot 1F5F3 So ballotScriptX 1F5F4 So ballotBoxScriptX 1F5F5 So ballotBoldScriptX 1F5F6 So ballotBoxBoldScriptX 1F5F7 So lightCheckMark 1F5F8 So ballotBoxBoldCheck 1F5F9 So worldMap 1F5FA So mountFuji 1F5FB So tokyoTower 1F5FC So statueOfLiberty 1F5FD So silhouetteOfJapan 1F5FE So moyai 1F5FF So # Emoticons grinningFace 1F600 So grinningFaceWithSmilingEyes 1F601 So faceWithTearsOfJoy 1F602 So smilingFaceWithOpenMouth 1F603 So smilingFaceWithOpenMouthAndSmilingEyes 1F604 So smilingFaceWithOpenMouthAndColdSweat 1F605 So smilingFaceWithOpenMouthAndTightlyClosedEyes 1F606 So smilingFaceWithHalo 1F607 So smilingFaceWithHorns 1F608 So winkingFace 1F609 So smilingFaceWithSmilingEyes 1F60A So faceSavouringDeliciousFood 1F60B So relievedFace 1F60C So smilingFaceWithHeartShapedEyes 1F60D So smilingFaceWithSunglasses 1F60E So smirkingFace 1F60F So neutralFace 1F610 So expressionlessFace 1F611 So unamusedFace 1F612 So faceWithColdSweat 1F613 So pensiveFace 1F614 So confusedFace 1F615 So confoundedFace 1F616 So kissingFace 1F617 So faceThrowingAKiss 1F618 So kissingFaceWithSmilingEyes 1F619 So kissingFaceWithClosedEyes 1F61A So faceWithStuckOutTongue 1F61B So faceWithStuckOutTongueAndWinkingEye 1F61C So faceWithStuckOutTongueAndTightlyClosedEyes 1F61D So disappointedFace 1F61E So worriedFace 1F61F So angryFace 1F620 So poutingFace 1F621 So cryingFace 1F622 So perseveringFace 1F623 So faceWithLookOfTriumph 1F624 So disappointedButRelievedFace 1F625 So frowningFaceWithOpenMouth 1F626 So anguishedFace 1F627 So fearfulFace 1F628 So wearyFace 1F629 So sleepyFace 1F62A So tiredFace 1F62B So grimacingFace 1F62C So loudlyCryingFace 1F62D So faceWithOpenMouth 1F62E So hushedFace 1F62F So faceWithOpenMouthAndColdSweat 1F630 So faceScreamingInFear 1F631 So astonishedFace 1F632 So flushedFace 1F633 So sleepingFace 1F634 So dizzyFace 1F635 So faceWithoutMouth 1F636 So faceWithMedicalMask 1F637 So grinningCatFaceWithSmilingEyes 1F638 So catFaceWithTearsOfJoy 1F639 So smilingCatFaceWithOpenMouth 1F63A So smilingCatFaceWithHeartShapedEyes 1F63B So catFaceWithWrySmile 1F63C So kissingCatFaceWithClosedEyes 1F63D So poutingCatFace 1F63E So cryingCatFace 1F63F So wearyCatFace 1F640 So slightlyFrowningFace 1F641 So slightlySmilingFace 1F642 So upsideDownFace 1F643 So faceWithRollingEyes 1F644 So faceWithNoGoodGesture 1F645 So faceWithOkGesture 1F646 So personBowingDeeply 1F647 So seeNoEvilMonkey 1F648 So hearNoEvilMonkey 1F649 So speakNoEvilMonkey 1F64A So happyPersonRaisingOneHand 1F64B So personRaisingBothHandsInCelebration 1F64C So personFrowning 1F64D So personWithPoutingFace 1F64E So personWithFoldedHands 1F64F So # Transport and Map Symbols rocket 1F680 So helicopter 1F681 So steamLocomotive 1F682 So railwayCar 1F683 So highSpeedTrain 1F684 So highSpeedTrainWithBulletNose 1F685 So train 1F686 So metro 1F687 So lightRail 1F688 So station 1F689 So tram 1F68A So tramCar 1F68B So bus 1F68C So oncomingBus 1F68D So trolleybus 1F68E So busStop 1F68F So minibus 1F690 So ambulance 1F691 So fireEngine 1F692 So policeCar 1F693 So oncomingPoliceCar 1F694 So taxi 1F695 So oncomingTaxi 1F696 So automobile 1F697 So oncomingAutomobile 1F698 So recreationalVehicle 1F699 So deliveryTruck 1F69A So articulatedLorry 1F69B So tractor 1F69C So monorail 1F69D So mountainRailway 1F69E So suspensionRailway 1F69F So mountainCableway 1F6A0 So aerialTramway 1F6A1 So ship 1F6A2 So rowboat 1F6A3 So speedboat 1F6A4 So horizontalTrafficLight 1F6A5 So verticalTrafficLight 1F6A6 So constructionSign 1F6A7 So policeCarsRevolvingLight 1F6A8 So triangularFlagOnPost 1F6A9 So door 1F6AA So noEntrySign 1F6AB So smoking 1F6AC So noSmoking 1F6AD So putLitterInItsPlace 1F6AE So doNotLitter 1F6AF So potableWater 1F6B0 So nonPotableWater 1F6B1 So bicycle 1F6B2 So noBicycles 1F6B3 So bicyclist 1F6B4 So mountainBicyclist 1F6B5 So pedestrian 1F6B6 So noPedestrians 1F6B7 So childrenCrossing 1F6B8 So mens 1F6B9 So womens 1F6BA So restroom 1F6BB So trns:baby 1F6BC So toilet 1F6BD So waterCloset 1F6BE So shower 1F6BF So bath 1F6C0 So bathtub 1F6C1 So passportControl 1F6C2 So customs 1F6C3 So baggageClaim 1F6C4 So leftLuggage 1F6C5 So triangleWithRoundedCorners 1F6C6 So prohibitedSign 1F6C7 So circledInformationSource 1F6C8 So boys 1F6C9 So girls 1F6CA So couchAndLamp 1F6CB So sleepingAccommodation 1F6CC So shoppingBags 1F6CD So bellhopBell 1F6CE So bed 1F6CF So placeOfWorship 1F6D0 So octagonalSign 1F6D1 So shoppingTrolley 1F6D2 So stupa 1F6D3 So pagoda 1F6D4 So hinduTemple 1F6D5 So hammerAndWrench 1F6E0 So shield 1F6E1 So oilDrum 1F6E2 So motorway 1F6E3 So railwayTrack 1F6E4 So motorBoat 1F6E5 So upPointingMilitaryAirplane 1F6E6 So upPointingAirplane 1F6E7 So upPointingSmallAirplane 1F6E8 So smallAirplane 1F6E9 So northeastPointingAirplane 1F6EA So airplaneDeparture 1F6EB So airplaneArriving 1F6EC So satellite 1F6F0 So oncomingFireEngine 1F6F1 So dieselLocomotive 1F6F2 So passengerShip 1F6F3 So scooter 1F6F4 So motorScooter 1F6F5 So canoe 1F6F6 So sled 1F6F7 So flyingSaucer 1F6F8 So skateboard 1F6F9 So autoRickshaw 1F6FA So ================================================ FILE: packages/coldtype-core/src/coldtype/axidraw.py ================================================ from coldtype.pens.axidrawpen import AxiDrawPen from coldtype.runon.path import P from coldtype.renderable import renderable from coldtype.geometry import Rect, Point from time import sleep try: from pyaxidraw import axidraw except: print("Couldn’t import pyaxidraw") print("https://axidraw.com/doc/py_api/#installation") print("pip install https://cdn.evilmadscientist.com/dl/ad/public/AxiDraw_API.zip") def aximeta(fn): def _aximeta(pen:P): pen.data(aximeta=dict(fn=fn)) return _aximeta def dip_pen(seconds=1, location=(0, 0)): return (P() .ch(aximeta(lambda ad: ad .moveto(*location) .pendown() .sleep(seconds) .penup() .moveto(0, 0)))) class AxidrawChainable(): def __init__(self, ad): self.ad = ad def moveto(self, x, y): self.ad.moveto(x, y) return self def penup(self): self.ad.penup() return self def pendown(self): self.ad.pendown() return self def sleep(self, t): sleep(t) return self class axidrawing(renderable): def __init__(self, vertical=False, flatten=10, **kwargs ): self.flatten = flatten self.vertical = vertical if self.vertical: super().__init__(rect=(850, 1100), **kwargs) else: super().__init__(rect=(1100, 850), **kwargs) def runpost(self, result, render_pass, renderer_state, config): def normalize(p, pos, data): if pos != 0: return if self.flatten: p.flatten(self.flatten, segmentLines=False) s = p.s() if not s or (s and s.a == 0): p.fssw(-1, 0, 3) else: p.fssw(-1, s, 3) res = (super() .runpost(result, render_pass, renderer_state, config) .walk(normalize)) return res def draw(self, tag=None, flatten=None, frame=0, test=False, speed_pendown=100, speed_penup=100, pen_rate_raise=100, pen_rate_lower=100, pen_delay_down=0, move_delay=0, ): def _draw(_): ad = None def walker(p:P, pos, _): if pos == 0: ameta = p.data("aximeta") if ameta: fn = ameta.get("fn") if fn: fn(AxidrawChainable(ad)) return p = p.cond(flatten, lambda p: p.flatten( flatten, segmentLines=False)) ap = AxiDrawPen(p, Rect(0, 0, 1100, 850)) ap.draw(ad=ad, move_delay=move_delay, zero=False) res = self.frame_result(frame, post=True) if self.vertical: res = res.copy().rotate(90, point=Point(0, 0)).translate(1100, 0) if tag is not None: if isinstance(tag, int): res = res[tag].copy(with_data=True) else: res = res.find_(tag).copy(with_data=True) if test: print("-"*30) print("AXIDRAW TEST") print(">", res) print("-"*30) else: ad = axidraw.AxiDraw() ad.interactive() ad.options.units = 0 ad.options.speed_pendown = speed_pendown ad.options.speed_penup = speed_penup ad.options.pen_rate_raise = pen_rate_raise ad.options.pen_rate_lower = pen_rate_lower ad.options.pen_delay_down = pen_delay_down ad.connect() print("connected/") ad.penup() ad.moveto(0,0) try: res.walk(walker) except Exception as e: print(">>>", e) finally: ad.penup() ad.moveto(0,0) ad.disconnect() print("/disconnected") return _draw ================================================ FILE: packages/coldtype-core/src/coldtype/beziers.py ================================================ import math from re import sub from fontTools.pens.recordingPen import RecordingPen, replayRecording from fontTools.misc.bezierTools import calcCubicArcLength, splitCubicAtT, calcQuadraticArcLength, splitQuadraticAtT from coldtype.geometry import Rect, Point, Line def raise_quadratic(start, a, b): c0 = start c1 = (c0[0] + (2/3)*(a[0] - c0[0]), c0[1] + (2/3)*(a[1] - c0[1])) c2 = (b[0] + (2/3)*(a[0] - b[0]), b[1] + (2/3)*(a[1] - b[1])) c3 = (b[0], b[1]) return [c1, c2, c3] __length_cache = {} __split_cache = {} def splitCubicAtT_cached(a, b, c, d, t): global __split_cache abcdt = (a, b, c, d, t) sc = __split_cache.get(abcdt) if sc: return sc else: s = splitCubicAtT(a, b, c, d, t) __split_cache[abcdt] = s return s def splitQuadraticAtT_cached(a, b, c, t): global __split_cache abct = (a, b, c, t) sc = __split_cache.get(abct) if sc: return sc else: s = splitQuadraticAtT(a, b, c, t) __split_cache[abct] = s return s def calcCubicArcLength_cached(a, b, c, d): #return calcCubicArcLength(a, b, c, d) global __length_cache abcd = (a, b, c, d) lc = __length_cache.get(abcd) if lc: return lc else: l = calcCubicArcLength(a, b, c, d) __length_cache[abcd] = l return l def calcQuadraticArcLength_cached(a, b, c): global __length_cache abc = (a, b, c) lc = __length_cache.get(abc) if lc: return lc else: l = calcQuadraticArcLength(a, b, c) __length_cache[abc] = l return l class CurveCutter(): def __init__(self, g, inc=0.0015): self.pen = RecordingPen() if hasattr(g, "value"): self.pen = g.copy() else: g.replay(self.pen) #g.draw(self.pen) #self.pen.ensure_fully_closed_path() self.inc = inc self.length = self.calcCurveLength() def calcCurveLength(self): length = 0 for i, (t, pts) in enumerate(self.pen.value): if t == "curveTo": p1, p2, p3 = pts p0 = self.pen.value[i-1][-1][-1] length += calcCubicArcLength_cached(p0, p1, p2, p3) elif t == "qCurveTo": p1, p2 = pts p0 = self.pen.value[i-1][-1][-1] length += calcQuadraticArcLength_cached(p0, p1, p2) elif t == "lineTo": p0 = self.pen.value[i-1][-1][-1] p1, = pts l = Line(p0, p1) length += l.length() return length def subsegment(self, start=None, end=None): global __cut_cache inc = self.inc length = self.length ended = False _length = 0 out = [] for i, (t, pts) in enumerate(self.pen.value): if t == "curveTo": p1, p2, p3 = pts p0 = self.pen.value[i-1][-1][-1] length_arc = calcCubicArcLength_cached(p0, p1, p2, p3) if _length + length_arc < end: _length += length_arc else: t = inc tries = 0 while not ended: a, b = splitCubicAtT_cached(p0, p1, p2, p3, t) length_a = calcCubicArcLength_cached(*a) if _length + length_a > end: ended = True out.append(("curveTo", a[1:])) else: t += inc tries += 1 elif t == "qCurveTo": p1, p2 = pts p0 = self.pen.value[i-1][-1][-1] length_arc = calcQuadraticArcLength_cached(p0, p1, p2) if _length + length_arc < end: _length += length_arc else: t = inc tries = 0 while not ended: a, b = splitQuadraticAtT_cached(p0, p1, p2, t) length_a = calcQuadraticArcLength_cached(*a) if _length + length_a > end: ended = True out.append(("qCurveTo", a[1:])) else: t += inc tries += 1 elif t == "lineTo": p0 = self.pen.value[i-1][-1][-1] p1, = pts l = Line(p0, p1) length_line = l.length() if ended: pass elif _length + length_line < end: _length += length_line else: res = l.tpx(end - _length) out.append(("lineTo", (res,))) ended = True if not ended: out.append((t, pts)) if out[-1][0] != "endPath": out.append(("endPath",[])) return out def subsegmentPoint(self, start=0, end=1): subsegment = self.subsegment(start=start, end=end) try: t, ps = subsegment[-2] if t == "lineTo": a = subsegment[-3][1][0] b, = ps tangent = math.degrees(math.atan2(b[1] - a[1], b[0] - a[0]) + math.pi*.5) return Point(b), tangent elif t == "curveTo": t, (a, b, c) = subsegment[-2] tangent = math.degrees(math.atan2(c[1] - b[1], c[0] - b[0]) + math.pi*.5) return Point(c), tangent elif t == "qCurveTo": t, (a, b) = subsegment[-2] tangent = math.degrees(math.atan2(b[1] - a[1], b[0] - a[0]) + math.pi*.5) return Point(b), tangent except ValueError as e: print(e) return None, None class CurveSample(): def __init__(self, idx, pt, e, tan): self.idx = idx self.pt = pt self.e = e self.tan = tan def neighbors(self, prev, next): self.prev = prev self.next = next #self.difft = ((next-prev) + 180) % 360 - 180 ================================================ FILE: packages/coldtype-core/src/coldtype/blender/__init__.py ================================================ # to be loaded from within Blender from enum import Enum import os, math, json, time from pathlib import Path from coldtype.geometry import curve from coldtype.geometry.rect import Rect from coldtype.runon.path import P from coldtype.pens.blenderpen import BlenderPen, BlenderPenCube, BPH from coldtype.color import hsl from coldtype.timing import Frame, Timeline, Timeable from coldtype.renderable import renderable, Overlay, Action, runnable from coldtype.renderable.animation import animation from coldtype.blender.render import blend_source from coldtype.timing.sequence import ClipTrack, Clip, Sequence from coldtype.blender.fluent import BpyWorld, BpyObj, BpyCollection, BpyGroup, BpyMaterial from typing import Callable try: import bpy # noqa except ImportError: bpy = None pass class BlenderIO(): def __init__(self, file): file = Path(str(file)).expanduser() #file.parent.mkdir(exist_ok=True, parents=True) self.blend_file = file self.data_file = Path(str(file) + ".json") def data(self): if self.data_file.exists(): return json.loads(self.data_file.read_text()) class BlenderTimeline(Timeline): def __init__(self, file, duration=None, fps=30, **kwargs): if isinstance(file, BlenderIO): file = file.data_file self.file = file if not self.file.exists(): self.file.parent.mkdir(parents=True, exist_ok=True) self.file.write_text('{}') try: data = json.loads(file.read_text()) except: data = dict() timeables = [] for track in data.get("tracks", []): for tidx, t in enumerate(track["clips"]): timeables.append(Timeable( t["start"], t["end"], name=t["text"], index=tidx, track=track["index"])) self.livepreview_disabled = bool(data.get("livepreview_disabled", False)) self.workarea_set = bool(data.get("workarea_set", 0)) self.workarea = range( data.get("start", 0), data.get("end", 30)+1) super().__init__( fps=fps #data.get("fps", 30) , timeables=timeables , start=0 , end=duration or data.get("end")+1 , **kwargs) def b3d(callback:Callable[[BlenderPen], BlenderPen], collection="Coldtype", primitive=None, dn=False, cyclic=True, material=None, zero=False, upright=False, tag_prefix=None, ): if not bpy: # short-circuit if this is meaningless return lambda x: x pen_mod = None if callback and not callable(callback): pen_mod = callback[0] callback = callback[1] def annotate(pen:P): if bpy and bpy.data and pen_mod: pen_mod(pen) prev = pen.data("b3d", {}) if prev: callbacks = [*prev.get("callbacks"), callback] else: callbacks = [callback] #c = None #if zero: # c = pen.ambit().pc # pen.translate(-c.x, -c.y) pen.data(b3d=dict( collection=(collection or prev.get("collection", "Coldtype")), callbacks=callbacks, material=(material or prev.get("material", "ColdtypeDefault")), tag_prefix=(tag_prefix or prev.get("tag_prefix")), dn=dn, cyclic=cyclic, primitive=primitive, zero=zero, #reposition=c, upright=upright)) return annotate def b3d_post(callback:Callable[[BlenderPen], BlenderPen]): if not bpy: # short-circuit for non-bpy return lambda x: x def _b3d_post(pen:P): prev = pen.data("b3d_post") if prev: callbacks = [*prev, callback] else: callbacks = [callback] pen.data(b3d_post=callbacks) return _b3d_post def b3d_pre(callback:Callable[[P], P]): def _cast(pen:P): if bpy and bpy.data: callback(pen) return _cast def walk_to_b3d(result:P, dn=False, renderable=None, ): built = {} center = renderable.center center_rect = renderable.rect def walker(p:P, pos, data): bp = None if pos == 0: bdata = p.data("b3d") if not bdata: p.ch(b3d(lambda bp: bp.extrude(0.01))) bdata = p.data("b3d") zero = bdata.get("zero", False) if center and True: cx = -center_rect.w/2*(1-center[0]) cy = -center_rect.h/2*(1-center[1]) p.translate(cx, cy) pc = p.ambit().pc if zero: p.translate(-pc.x, -pc.y) if p.tag() is None: tag = data["utag"] if bdata.get("tag_prefix"): tag = bdata.get("tag_prefix") + tag else: tag = "ct_autotag_" + tag p.tag(tag) if bdata: coll = BPH.Collection(bdata["collection"]) material = bdata.get("material", "ColdtypeDefault") if len(p.v.value) == 0: p.hide() denovo = bdata.get("dn", dn) cyclic = bdata.get("cyclic", True) primitive = bdata.get("primitive") if primitive is not None: _class = BlenderPen if primitive == "cube": _class = BlenderPenCube bp = p.cast(_class).draw(coll, primitive=primitive, material=material, dn=True) else: bp = p.cast(BlenderPen).draw(coll, dn=denovo, material=material, cyclic=cyclic) bp.rotate(0) if bdata.get("callbacks"): for cb in bdata.get("callbacks"): cb(bp) bp.hide(not p._visible) # if center and False: # cx = -center_rect.w/2*(1-center[0]) # cy = -center_rect.h/2*(1-center[1]) # bp.locate_relative(cx/100, cy/100) if renderable: if renderable.upright: bp.rotate(90) if zero: #bdata.get("reposition"): pt = pc #bdata.get("reposition") if bdata.get("upright"): bp.locate_relative(pt.x/100, 0, pt.y/100) else: bp.locate_relative(pt.x/100, pt.y/100) built[p.tag()] = (p, bp) if pos == 0 or pos == 1: b3d_post = p.data("b3d_post") if b3d_post: for post in b3d_post: post(bp) result.walk(walker) class B3DPlayback(Enum): AlwaysStop = 0 AlwaysPlay = 1 KeepPlaying = 2 class b3d_runnable(runnable): def __init__(self, solo=False, cond=None, once=True, delay=False, playback:B3DPlayback=B3DPlayback.AlwaysStop, force_refresh=False, ): self.once = once self.delay = delay self.playback = playback self.force_refresh = force_refresh if cond is not None: super().__init__(solo=solo, cond=lambda: cond and bool(bpy) and bool(bpy.data)) else: super().__init__(solo=solo, cond=lambda: bool(bpy) and bool(bpy.data)) def run(self): if not bpy: return None else: return self.func(BpyWorld().deselect_all()) class b3d_renderable(renderable): def __init__(self, rect=(1080, 1080), center=(0, 0), upright=False, post_run=None, reset_to_zero=False, force_refresh=False, **kwargs ): self.center = center self.upright = upright self.post_run = post_run self.blender_io:BlenderIO = None self.reset_to_zero = reset_to_zero self.force_refresh = force_refresh super().__init__(rect, **kwargs) class b3d_animation(animation): def __init__(self, rect=(1080, 1080), samples=-1, denoise=False, match_length=True, match_output=True, match_fps=True, bake=False, center=(0, 0), upright=False, autosave=False, renderer="b3d", force_refresh=False, **kwargs ): self.func = None self.name = None self.current_frame = -1 self.samples = samples self.denoise = denoise self.bake = bake self.center = center self.upright = upright self.match_length = match_length self.match_output = match_output self.match_fps = match_fps self.renderer = renderer self.autosave = autosave self.force_refresh = force_refresh self._bt = False self.blender_io:BlenderIO = None if "timeline" not in kwargs: kwargs["timeline"] = Timeline(30) super().__init__(rect=rect, **kwargs) do_match_length = self.match_length if bpy and bpy.data and do_match_length and bpy: scene = bpy.data.scenes[0] if hasattr(self.timeline, "workarea_set") and self.timeline.workarea_set: pass else: scene.frame_start = 0 scene.frame_end = self.t.duration-1 if bpy and bpy.data and self.match_fps: # don't think this is totally accurate but good enough for now if isinstance(self.t.fps, float): bpy.data.scenes[0].render.fps = round(self.t.fps) bpy.data.scenes[0].render.fps_base = 1.001 else: bpy.data.scenes[0].render.fps = self.t.fps bpy.data.scenes[0].render.fps_base = 1 if isinstance(self.timeline, BlenderTimeline): self.add_watchee(self.timeline.file) def post_read(self): out = super().post_read() if bpy and bpy.data and self.match_output: bpy.data.scenes[0].render.filepath = str(self.pass_path(index=None)) return out def running_in_viewer(self): return not bpy or bpy.data def rasterize(self, config, content, rp): if self.renderer == "skia": return super().rasterize(config, content, rp) try: from b3denv import get_vars b3d_vars = get_vars(None) blender_path = Path(b3d_vars["blender"]) except: raise Exception("NO BLENDER FOUND (via b3denv)") fi = rp.args[0].i blend_source(blender_path, self.filepath, self.blender_io.blend_file, fi, self.pass_path(index=None), self.samples, denoise=self.denoise) return True def baked_frames(self): def bakewalk(p, pos, data): if pos == 0: fi = data["idx"][0] (p.ch(b3d(callback=lambda bp: bp .show_on_frame(fi) .print(f"Baking frame {fi}..."), collection=f"CTBakedAnimation_{self.name}", dn=True, tag_prefix=f"ct_baked_frame_{fi}_{self.name}__"))) to_bake = P() for ps in self.passes(Action.RenderAll, None)[:]: to_bake += self.run_normal(ps, None) return to_bake.walk(bakewalk) class b3d_sequencer(b3d_animation): def __init__(self, rect=Rect(1080, 1080), autosave=True, in_blender=False, match_output=False, live_preview_scale=0.25, timeline:BlenderTimeline=None, **kwargs ): self.live_preview = not timeline.livepreview_disabled self.live_preview_scale = live_preview_scale super().__init__( rect=rect, timeline=timeline, match_fps=True, match_length=True, match_output=match_output, autosave=autosave, renderer="b3d" if in_blender else "skia", **kwargs) ================================================ FILE: packages/coldtype-core/src/coldtype/blender/fluent.py ================================================ import math, time, re from pathlib import Path from typing import Callable from contextlib import contextmanager from typing import List from coldtype.runon.path import P from coldtype.runon.runon import Runon from coldtype.timing.timeline import Timeline from coldtype.geometry import Rect from coldtype.color import Gradient, normalize_color, bw, Color from coldtype.renderable.animation import animation try: import bpy from mathutils import Vector, Euler except ImportError: bpy = None pass # ensure_channelbag & get_fcurves are taken/modified from # https://blenderartists.org/t/how-to-access-fcurves-in-blender-5-0/1623022/6 def ensure_channelbag(data_block): """ Returns the channelbag of f-curves for a given ID, or `None` if the ID doesn't have an animation data, an action, or a slot. """ if data_block.animation_data is None: return None anim_data = data_block.animation_data if anim_data.action is None: return None action = anim_data.action if action.is_empty: return None if anim_data.action_slot is None: return None try: from bpy_extras.anim_utils import action_ensure_channelbag_for_slot return action_ensure_channelbag_for_slot(action, anim_data.action_slot) except ImportError: #print("could not import action_ensure_channelbag_for_slot") pass def get_fcurves(obj, matching: re.Pattern = None): if not obj.animation_data: return None fcurves_out = [] # --- Keyframed FCurves (action) --- if obj.animation_data.action: if hasattr(obj.animation_data.action, 'fcurves'): fcurves = obj.animation_data.action.fcurves else: channelbag = ensure_channelbag(obj) if channelbag is not None: fcurves = channelbag.fcurves else: fcurves = [] for fcurve in fcurves: if matching is None or re.match(matching, fcurve.data_path): fcurves_out.append(fcurve) # --- Driver FCurves --- for fcurve in obj.animation_data.drivers: if matching is None or re.match(matching, fcurve.data_path): fcurves_out.append(fcurve) return fcurves_out if fcurves_out else None # TODO easy, chainable interface for # blender objects (could be a separate library) def set_b3d_color(value, color): color = normalize_color(color) value[0] = color.r value[1] = color.g value[2] = color.b value[3] = 1 class _Chainable(): def noop(self): return self def op(self, fn): fn(self) return self def data(self, key=None, default=None, **kwargs): if not hasattr(self, "_data"): self._data = {} if key is None and len(kwargs) > 0: for k, v in kwargs.items(): self._data[k] = v return self elif key is not None: return self._data.get(key, default) else: return self class BpyWorld(_Chainable): def __init__(self, scene="Scene"): try: self.scene = bpy.data.scenes[scene] except Exception as _: self.scene = None def deselect_all(self): active = bpy.context.view_layer.objects.active if active and active.mode == "EDIT": bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') return self deselectAll = deselect_all def delete_previous(self, collection="Coldtype", keep=[], materials=True, curves=True, meshes=True, objects=True, grease_pencils=True) -> "BpyWorld": self.deselect_all() BpyCollection.Find(collection, create=False).delete_hierarchy() self.deleteOrphans(keep=keep, materials=materials, curves=curves, meshes=meshes, objects=objects, grease_pencils=grease_pencils) return self deletePrevious = delete_previous def deleteOrphans(self, keep=[], **kwargs): from bpy import data as D props = ["curves", "meshes", "materials", "objects", "grease_pencils"] for x in range(2): for c in D.collections: if ("RigidBodyWorld" in c.name or c.users == 0) and c.name not in keep: bpy.data.collections.remove(c) for p in props: if kwargs.get(p): for block in getattr(D, p): #print(">>", getattr(D, p)) if block.users == 0 and block.name not in keep: getattr(D, p).remove(block) return self def timeline(self, t:Timeline, resetFrame=None, output=None, version=None): self.scene.frame_start = 0 self.scene.frame_end = t.duration-1 if isinstance(t.fps, float): self.scene.render.fps = round(t.fps) self.scene.render.fps_base = 1.001 else: self.scene.render.fps = t.fps self.scene.render.fps_base = 1 if resetFrame is not None: self.scene.frame_set(resetFrame) if output: output = Path(output) if output.is_file(): folder = output.stem if version: folder = f"{output.stem}_{version}" output = output.parent / "renders" / folder / f"{folder}_" self.scene.render.filepath = str(output) return self def cycles(self, samples=16, denoiser=False, canvas:Rect=None, transparent=False): self.scene.render.engine = "CYCLES" if samples > 0: self.scene.cycles.samples = samples if denoiser: self.scene.cycles.denoiser = "OPENIMAGEDENOISE" if denoiser == True else denoiser self.scene.cycles.use_denoising = True else: if denoiser is False: self.scene.cycles.use_denoising = False if canvas is not None: self.scene.render.resolution_x = round(canvas.w) self.scene.render.resolution_y = round(canvas.h) if transparent: self.scene.render.film_transparent = True return self render_settings = cycles renderSettings = render_settings #@contextmanager def rigidbody(self, speed=1, frame_end=250): try: bpy.ops.rigidbody.world_remove() #yield except RuntimeError as e: print("! Failed to reset rigidbody !", e) #yield if self.scene: try: bpy.ops.rigidbody.world_add() except RuntimeError: pass rw = self.scene.rigidbody_world if rw: rw.time_scale = speed rw.point_cache.frame_end = frame_end return self def insert_keyframe(self, frame, path, value=None): bpy.data.scenes[0].frame_set(frame) if value is not None: if callable(value): value(self) else: exec(f"self.scene.{path} = {value}") self.scene.keyframe_insert(data_path=path) return self #setattr(self.obj, path, value) def background(self, color): bg = bpy.data.worlds["World"].node_tree.nodes["Background"].inputs[0].default_value set_b3d_color(bg, color) return self class BpyCollection(_Chainable): @staticmethod def Find(tag, create=True, parent=None): if "/" in tag: if tag.startswith("/"): parentTag = "Coldtype" tag = tag[1:] else: parentTag, tag = tag.split("/") parent = BpyCollection.Find(parentTag, create=create) bc = BpyCollection() c = None if tag not in bpy.data.collections: if create: c = bpy.data.collections.new(tag) if parent: if isinstance(parent, BpyCollection): parent = parent.c parent.children.link(c) else: bpy.context.scene.collection.children.link(c) c = bpy.data.collections.get(tag) bc.c = c return bc # try: # bc.c = bpy.data.collections[tag] # except KeyError: # if create: # print("CREATING", tag) # bc.c = bpy.data.collections.new(tag) # else: # bc.c = None # return bc def delete_hierarchy(self): if not self.c: return bpy.context.view_layer.objects.active = None for obj in self.c.objects: try: BpyObj.Find(obj.name).select() except Exception as e: print("deleteHierarchy failed to delete object:", obj.name, e) bpy.ops.object.delete() bpy.data.collections.remove(self.c) return None deleteHierarchy = delete_hierarchy class BpyMaterial(): def __init__(self, material): self.m = material def Find(tag, create=True, use_nodes=True): if not isinstance(tag, str): return BpyMaterial(tag) try: m = bpy.data.materials[tag] return BpyMaterial(m) except KeyError: if create: m = bpy.data.materials.new(tag) m.use_nodes = use_nodes return BpyMaterial(m) return None def bsdf(self): return self.m.node_tree.nodes["Principled BSDF"] def setColorValue(self, value, color): value[0] = color.r value[1] = color.g value[2] = color.b value[3] = 1 if color.a != 1: self.transmission(1) def f(self, color): if isinstance(color, Gradient): return self.f(color.stops[0][0]) else: color = normalize_color(color) bsdf = self.bsdf() if bsdf: dv = bsdf.inputs[0].default_value self.setColorValue(dv, color) return self fill = f def specular(self, amount=0.5): self.bsdf().inputs[12].default_value = amount return self def metallic(self, amount=1): self.bsdf().inputs[1].default_value = amount return self def roughness(self, amount=0.5): self.bsdf().inputs[2].default_value = amount return self def transmission(self, amount=1): self.bsdf().inputs[17].default_value = amount return self def emission(self, color, strength=1): self.setColorValue(self.bsdf().inputs[19].default_value, normalize_color(color)) self.bsdf().inputs[20].default_value = strength return self def animation(self, anim:animation, start=0): src = anim.pass_path(start) tl = anim.timeline return self.image(src, timeline=tl) def image(self, src=None, opacity=1, rect=None, pattern=True, alpha=True, timeline:Timeline=None, emission=None, render=False) -> "BpyObj": from coldtype.renderable import renderable, animation if isinstance(src, animation): timeline = src.timeline src = src.render_and_rasterize() if render else src.pass_path(0) elif isinstance(src, renderable): src = src.render_and_rasterize() if render else src.pass_path(0) src = Path(src) bsdf = self.bsdf() if "Image Texture" in self.m.node_tree.nodes: tex = self.m.node_tree.nodes["Image Texture"] else: tex = self.m.node_tree.nodes.new("ShaderNodeTexImage") self.m.node_tree.links.new(bsdf.inputs["Base Color"], tex.outputs["Color"]) if alpha: self.m.node_tree.links.new(bsdf.inputs["Alpha"], tex.outputs["Alpha"]) if emission: self.m.node_tree.links.new(bsdf.inputs["Emission"], tex.outputs["Color"]) bsdf.inputs[20].default_value = emission bx, by = bsdf.location tex.location = (bx - 320, by) found = None for img in bpy.data.images: if src.name in img.name or img.name == src.name: found = img img.reload() if found is None: print("NOT FOUND", src.name) print("exists?", src.exists()) found = bpy.data.images.load(str(src)) tex.image = found if timeline is not None: tex.image.source = "SEQUENCE" tex.image_user.frame_duration = timeline.duration tex.image_user.frame_start = 0 tex.image_user.frame_offset = -1 tex.image_user.use_cyclic = True tex.image_user.use_auto_refresh = True tex.interpolation = "Closest" tex.interpolation = "Linear" return self def color_ramp(self, colors:List[Color]): ramp = self.m.node_tree.nodes.new("ShaderNodeValToRGB") cr = ramp.color_ramp for idx, color in enumerate(colors): if idx == 0: cr.elements[0].color = color.rgba() elif idx == idx == (len(colors) - 1): cr.elements[-1].color = color.rgba() else: cr.elements.new(idx/(len(colors)-1)) cr.elements[idx].color = color.rgba() bsdf = self.bsdf() self.m.node_tree.links.new(bsdf.inputs["Base Color"], ramp.outputs["Color"]) return self def math(self, attrs={}, inputs=[]): m = self.m.node_tree.nodes.new("ShaderNodeMath") for k, v in attrs.items(): setattr(m, k, v) for k, v in inputs.items(): m.inputs[k].default_value = v return self def object_info(self): _ = self.m.node_tree.nodes.new("ShaderNodeObjectInfo") return self def connect(self, output, input): nt = self.m.node_tree self.m.node_tree.links.new( nt.nodes[input[0]].inputs[input[1]], nt.nodes[output[0]].outputs[output[1]]) return self def arrange(self): for idx, node in enumerate(reversed(self.m.node_tree.nodes)): node.location.x = -300 + idx * 200 return self class BpyGroup(Runon): def yields_wrapped(self): return False def map(self, fn:Callable[["BpyObj"], "BpyObj"]): return super().map(fn) def deselect(self): return self.map(lambda bp: bp.select(False)) @staticmethod def Curves(pens:P, prefix=None, collection=None, cyclic=True, fill=True, tx=0, ty=0): curves = BpyGroup() curves.prefix = prefix def walker(p:P, pos, data): if pos == 0: name = None if prefix: name = prefix + "_" + ".".join([str(s) for s in data["idx"]]) curves.append(BpyObj.Curve(name=name, collection=collection).draw(p, cyclic=cyclic, fill=fill, tx=tx, ty=ty)) pens.walk(walker) return curves def copy(self, new_prefix): new_curves = BpyGroup() new_curves.prefix = new_prefix for bp in self: new_curves.append(bp.copy(self.prefix, new_prefix)) return new_curves class BpyObj(_Chainable): def __init__(self, obj=None) -> None: if obj: self.obj = obj self.eo = None @staticmethod def Find(tag): bobj = BpyObj() if isinstance(tag, BpyObj): bobj.obj = tag.obj elif isinstance(tag, bpy.types.Object): bobj.obj = tag else: try: bobj.obj = bpy.data.objects[tag] except KeyError: bobj.obj = None return bobj @staticmethod def Norm(obj_or_tag): if isinstance(obj_or_tag, BpyObj): return obj_or_tag else: return BpyObj.Find(obj_or_tag) @staticmethod def Primitive(name=None, collection="Coldtype", created_obj=None) -> "BpyObj": if collection is None: collection = "Coldtype" if collection == "Global": collection = None if created_obj: created = created_obj else: created = bpy.context.object bobj = BpyObj() bobj.obj = created if collection: bobj.collect(collection) created.select_set(False) if name is not None: bobj.obj.name = name return bobj @staticmethod def Empty(name=None, collection=None) -> "BpyObj": bpy.ops.object.empty_add(type="PLAIN_AXES") return BpyObj.Primitive(name, collection) @staticmethod def Cube(name=None, collection=None) -> "BpyObj": bpy.ops.mesh.primitive_cube_add() return BpyObj.Primitive(name, collection) @staticmethod def Circle(name=None, collection=None) -> "BpyObj": bpy.ops.mesh.primitive_circle_add() return BpyObj.Primitive(name, collection) @staticmethod def Plane(name=None, collection=None) -> "BpyObj": bpy.ops.mesh.primitive_plane_add() return BpyObj.Primitive(name, collection) @staticmethod def Curve(name=None, collection=None) -> "BpyObj": bpy.ops.curve.primitive_bezier_curve_add() bo = BpyObj.Primitive(name, collection) return bo @staticmethod def Cylinder(name=None, collection=None) -> "BpyObj": bpy.ops.mesh.primitive_cylinder_add() return BpyObj.Primitive(name, collection) @staticmethod def UVSphere(name=None, collection=None) -> "BpyObj": bpy.ops.mesh.primitive_uv_sphere_add() return BpyObj.Primitive(name, collection) @staticmethod def Monkey(name=None, collection=None) -> "BpyObj": bpy.ops.mesh.primitive_monkey_add() return BpyObj.Primitive(name, collection) @staticmethod def GPencil(name, p:P=None, stroke=bw(0, 0.5), fill=None, collection=None): gpencil_data = bpy.data.grease_pencils.new(name) gpencil = bpy.data.objects.new(gpencil_data.name, gpencil_data) gp = BpyObj.Primitive(name, collection, created_obj=gpencil) gp_layer = gp.obj.data.layers.new("lines") gp_frame = gp_layer.frames.new(bpy.context.scene.frame_current) gp_stroke = gp_frame.strokes.new() gp_stroke.line_width = 8 gp_stroke.start_cap_mode = 'ROUND' gp_stroke.end_cap_mode = 'ROUND' gp_stroke.use_cyclic = True if p is not None: pts = p.point_list() gp_stroke.points.add(len(pts)) for idx, pt in enumerate(pts): gp_stroke.points[idx].co = (pt.x, pt.y, 0) gp_stroke.points[idx].pressure = 4 mat = bpy.data.materials.new(name=f"{gp.obj.name}_Material") bpy.data.materials.create_gpencil_data(mat) gp.obj.data.materials.append(mat) mat.grease_pencil.show_fill = fill is not None if fill is not None: mat.grease_pencil.fill_color = fill.rgba() mat.grease_pencil.color = stroke.rgba() if stroke else None return gp def find_children(self): children = [] for o in bpy.data.objects: if o.parent == self.obj: children.append(o) return sorted(children, key=lambda c: c.name) def delete_recursively(self): for c in self.find_children(): bpy.data.objects.remove(c, do_unlink=True) bpy.data.objects.remove(self.obj, do_unlink=True) return None def collect(self, collectionTag, create=True, unlink=True, parent=None): bc = BpyCollection.Find(collectionTag, create=create, parent=parent) bc.c.objects.link(self.obj) if unlink: for c in self.obj.users_collection: if c != bc.c: c.objects.unlink(self.obj) return self def copy(self, old_prefix=None, new_prefix=None): new_obj = None with self.obj_selected(): bpy.ops.object.duplicate(linked=False) copied = bpy.context.object if old_prefix is not None and new_prefix is not None: copied.name = self.obj.name.replace(old_prefix, new_prefix) new_obj = BpyObj(copied).select(False) return new_obj def select(self, selected=True, set_active=False, clear_active=False): if selected: if set_active: if clear_active: bpy.context.view_layer.objects.active = None bpy.context.view_layer.objects.active = self.obj self.obj.select_set(True) else: self.obj.select_set(False) if set_active: bpy.context.view_layer.objects.active = None return self @contextmanager def obj_selected(self, yield_self=False, exit=True): if not self.obj: if yield_self: yield None else: yield return self bpy.context.view_layer.objects.active = None bpy.context.view_layer.objects.active = self.obj self.obj.select_set(True) if yield_self: yield self else: yield if exit: self.obj.select_set(False) bpy.context.view_layer.objects.active = None return self objSelected = obj_selected @contextmanager def obj_selection_sequence(self, other_tag): other = BpyObj.Norm(other_tag) if not self.obj or not other.obj: yield None return bpy.context.view_layer.objects.active = None self.obj.select_set(True) other.obj.select_set(True) bpy.context.view_layer.objects.active = other.obj yield other bpy.ops.object.parent_set(type="OBJECT") try: self.obj.select_set(False) except ReferenceError: pass try: other.obj.select_set(False) except ReferenceError: pass bpy.context.view_layer.objects.active = None return self objSelectionSequence = obj_selection_sequence @contextmanager def all_vertices_selected(self): with self.obj_selected(): bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_mode(type='VERT') bpy.ops.mesh.select_all(action='SELECT') yield bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.object.mode_set(mode='OBJECT') return self allVerticesSelected = all_vertices_selected @contextmanager def select_vertices(self, selector, keep_selected=False): with self.obj_selected(): bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_mode(type='VERT') bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.object.mode_set(mode='OBJECT') for idx, v in enumerate(self.obj.data.vertices): if selector(v): v.select = True else: pass bpy.ops.object.mode_set(mode='EDIT') yield if not keep_selected: bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.object.mode_set(mode='OBJECT') return self selectVertices = select_vertices def make_vertex_group(self, selector, name=None): with self.select_vertices(selector): bpy.ops.object.vertex_group_add() bpy.ops.object.vertex_group_assign() if name: self.obj.vertex_groups[-1].name = name return self makeVertexGroup = make_vertex_group def select_and_delete(self, select_mode, selector): with self.select_vertices(selector): bpy.ops.mesh.select_mode(type=select_mode) bpy.ops.mesh.delete(type=select_mode) return self selectAndDelete = select_and_delete def separate_by_loose_parts(self) -> BpyGroup: with self.all_vertices_selected(): bpy.ops.mesh.separate(type="LOOSE") bg = BpyGroup() bg.append(self) for obj in bpy.context.selected_objects: bg.append(BpyObj(obj)) bg.deselect() return bg def parent(self, parent_tag, hide=False): with self.obj_selection_sequence(parent_tag) as o: bpy.ops.object.parent_set(type="OBJECT") if hide: with o.obj_selected(): o.hide() return self def constrain_child_of(self , target:"BpyObj" , location=dict(x=1, y=1, z=1) , rotation=dict(x=0, y=0, z=0) , scale=dict(x=1, y=1, z=1) , influence=1 , clear=True ): if clear: for c in self.obj.constraints: self.obj.constraints.remove(c) self.ops_object("constraint_add", type="CHILD_OF") constraint = self.obj.constraints[0] constraint.target = target.obj for k, v in location.items(): setattr(constraint, f"use_location_{k}", bool(v)) for k, v in rotation.items(): setattr(constraint, f"use_rotation_{k}", bool(v)) for k, v in scale.items(): setattr(constraint, f"use_scale_{k}", bool(v)) constraint.influence = influence return self def hide(self, hide=True): self.obj.hide_viewport = hide self.obj.hide_render = hide return self def delete(self): if not self.obj: return None bpy.context.view_layer.objects.active = None bpy.context.view_layer.objects.active = self.obj self.obj.select_set(True) bpy.ops.object.delete() return None def set_prop(self, prop, value): setattr(self.obj, prop, value) return self def set_data(self, prop, value): setattr(self.obj.data, prop, value) return self def set_props(self, pairs): for prop, value in pairs: self.set_prop(prop, value) return self def calls(self, fn): # TODO call signature, but requires fake-bpy-module at top level, is that even possible? fn(self.obj) return self def set_frame(self, frame, scene=None): if scene is None: scene = bpy.data.scenes[0] scene.frame_set(frame) return self def insert_keyframe(self, frame, path, value=None, scene=None): self.set_frame(frame, scene) if value is not None: if callable(value): value(self) else: exec(f"self.obj.{path} = {value}") self.obj.keyframe_insert(data_path=path) return self def insert_keyframes(self, path, *settings): for frame, value in settings: self.insert_keyframe(frame, path, value) self.set_frame(settings[0][0]) return self def modify_keyframes(self, selector, action): fcurves = get_fcurves(self.obj, r".*") for fc in fcurves: matches = False if callable(selector) and selector(fc.data_path): matches = True elif selector == fc.data_path: matches = True if matches: action(fc) def make_keyframes_linear(self, selector): def make_linear(fc): fc.extrapolation = 'LINEAR' for kp in fc.keyframe_points: kp.handle_left_type = 'VECTOR' kp.handle_right_type = 'VECTOR' kp.handle_left = kp.co kp.handle_right = kp.co self.modify_keyframes(selector, make_linear) return self def set_visibility_at_frame(self, frame, visibility, scene=None): if scene is None: scene = bpy.data.scenes[0] scene.frame_set(frame) self.hide(not visibility) self.obj.keyframe_insert(data_path="hide_render") self.obj.keyframe_insert(data_path="hide_viewport") return self def show_on_frame(self, frame): self.set_visibility_at_frame(0, False) self.set_visibility_at_frame(frame, True) self.set_visibility_at_frame(frame+1, False) return self def show_at_frame(self, frame): self.set_visibility_at_frame(0, False) self.set_visibility_at_frame(frame-1, False) self.set_visibility_at_frame(frame, True) return self # Manipulation Methods def vertex_group_all(self): with self.all_vertices_selected(): bpy.ops.object.vertex_group_add() bpy.ops.object.vertex_group_assign() return self vertexGroupAll = vertex_group_all def add_empty_origin(self, collection="Coldtype"): bpy.ops.object.empty_add(type="PLAIN_AXES") bc = bpy.context.object bc.name = self.obj.name + "_EmptyOrigin" self.eo = bc BpyObj.Find(bc.name).collect(collection) return self addEmptyOrigin = add_empty_origin # Geometry Methods def apply_transform(self, location=True, rotation=True, scale=True, properties=True ): with self.obj_selected(): bpy.ops.object.transform_apply( location=location, rotation=rotation, scale=scale, properties=properties) return self applyTransform = apply_transform def apply_scale(self): return self.applyTransform(location=False, rotation=False, scale=True, properties=False) applyScale = apply_scale def apply_modifier(self, name): with self.obj_selected(): #bpy.ops.object.modifier_set_active(modifier=name) bpy.ops.object.modifier_apply(modifier=name) self.obj.to_mesh(preserve_all_data_layers=True) return self applyModifier = apply_modifier def applyAllModifiers(self): with self.obj_selected(): for mod in self.obj.modifiers: bpy.ops.object.modifier_apply(modifier=mod.name) self.obj.to_mesh(preserve_all_data_layers=True) return self #region Basic transformations def rotate(self, x=None, y=None, z=None): if isinstance(x, Euler): self.obj.rotation_euler = x return self if x is not None: self.obj.rotation_euler[0] = math.radians(x) if y is not None: self.obj.rotation_euler[1] = math.radians(y) if z is not None: self.obj.rotation_euler[2] = math.radians(z) return self def with_temp_origin(self, origin, fn): self.set_origin(*origin) fn(self) self.origin_to_geometry() return self def origin_to_geometry(self): with self.obj_selected(): bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') return self originToGeometry = origin_to_geometry def origin_to_cursor(self): with self.obj_selected(): bpy.ops.object.origin_set(type='ORIGIN_CURSOR') return self originToCursor = origin_to_cursor def set_origin(self, x, y, z): #saved_location = bpy.context.scene.cursor.location bpy.context.scene.cursor.location = Vector((x, y, z)) with self.obj_selected(): bpy.ops.object.origin_set(type='ORIGIN_CURSOR') bpy.context.scene.cursor.location = Vector((0, 0, 0)) return self setOrigin = set_origin def locate(self, x=None, y=None, z=None) -> "BpyObj": if isinstance(x, Vector): self.obj.location = x return self if x is not None: self.obj.location[0] = x if y is not None: self.obj.location[1] = y if z is not None: self.obj.location[2] = z return self def locate_relative(self, x=None, y=None, z=None): if x is not None: self.obj.location[0] = self.obj.location[0] + x if y is not None: self.obj.location[1] = self.obj.location[1] + y if z is not None: self.obj.location[2] = self.obj.location[2] + z if self.eo: (BpyObj.Find(self.eo.name) .locate_relative(x=x, y=y, z=z)) return self locateRelative = locate_relative def scale(self, x=None, y=None, z=None) -> "BpyObj": if x is not None: self.obj.scale[0] = x if y is not None: self.obj.scale[1] = y if z is not None: self.obj.scale[2] = z return self def dimension(self, x=None, y=None, z=None) -> "BpyObj": if x is not None: self.obj.dimensions[0] = x if y is not None: self.obj.dimensions[1] = y if z is not None: self.obj.dimensions[2] = z return self def dimensions(self, x=None, y=None, z=None) -> "BpyObj": for k, d in dict(x=x, y=y, z=z).items(): if d is not None: self.dimension(**{k:d}) self.applyScale() return self #endregion Basic transformations #region Materials def material(self, tag, modFn:Callable[[BpyMaterial], BpyMaterial]=None, clear=True): if clear: self.obj.data.materials.clear() bm = BpyMaterial.Find(tag, create=True, use_nodes=True) if bm.m.name not in self.obj.data.materials: self.obj.data.materials.append(bm.m) if bm and modFn: modFn(bm) return self #endregion Materials #region Convenience Methods def rigidbody(self, mode="active", animated=False, mesh=False, bounce=0, mass=1, bake=0, deactivated=False, friction=0.5, linear_damping=0.04, angular_damping=0.1 ): if not self.obj: return self with self.obj_selected(): o = self.obj bpy.ops.rigidbody.object_add() if mesh: o.rigid_body.collision_shape = "MESH" o.rigid_body.type = mode.upper() o.rigid_body.kinematic = animated o.rigid_body.restitution = bounce o.rigid_body.mass = mass o.rigid_body.friction = friction o.rigid_body.linear_damping = linear_damping o.rigid_body.angular_damping = angular_damping if deactivated: o.rigid_body.use_deactivation = True o.rigid_body.use_start_deactivated = True if bake: bpy.ops.rigidbody.bake_to_keyframes() return self #endregion Convenience Methods #region Modifiers #@contextmanager def modifier(self, name, cb=None, apply=True, **kwargs): with self.obj_selected(): bpy.ops.object.modifier_add(type=name.upper()) m = self.obj.modifiers[name] for k, v in kwargs.items(): setattr(m, k, v) if cb: cb(m) if apply: self.apply_modifier(name) return self def solidify(self, thickness=1): with self.obj_selected(): bpy.ops.object.modifier_add(type="SOLIDIFY") m = self.obj.modifiers["Solidify"] m.thickness = thickness return self def convert_to_mesh(self): with self.obj_selected(): bpy.ops.object.convert(target="MESH") return self convertToMesh = convert_to_mesh def remesh(self, octree_depth=7, smooth=False, apply=False): with self.obj_selected(): bpy.ops.object.modifier_add(type="REMESH") m = self.obj.modifiers["Remesh"] m.mode = "SHARP" m.octree_depth = octree_depth m.use_remove_disconnected = False m.use_smooth_shade = smooth if apply: self.apply_modifier("Remesh") return self def array(self, count=2, relative=(1, 0, 0), constant=(0.1, 0, 0)): if any(v is None for v in relative): relative = None if any(v is None for v in constant): constant = None with self.obj_selected(): bpy.ops.object.modifier_add(type='ARRAY') m = self.obj.modifiers[-1] m.count = count if relative: m.use_relative_offset = True m.relative_offset_displace[0] = relative[0] m.relative_offset_displace[1] = relative[1] m.relative_offset_displace[2] = relative[2] else: m.use_relative_offset = False if constant: m.use_constant_offset = True m.constant_offset_displace[0] = constant[0] m.constant_offset_displace[1] = constant[1] m.constant_offset_displace[2] = constant[2] else: m.use_constant_offset = False return self def arrayX(self, count=2, relative=1, constant=0): return self.array(count=count, relative=(relative, 0, 0), constant=(constant, 0, 0)) def arrayY(self, count=2, relative=-1, constant=0): return self.array(count=count, relative=(0, relative, 0), constant=(0, constant, 0)) def arrayZ(self, count=2, relative=-1, constant=0): return self.array(count=count, relative=(0, 0, relative), constant=(0, 0, constant)) def remove_doubles(self, threshold=0.01): with self.all_vertices_selected(): bpy.ops.mesh.remove_doubles(threshold=threshold) return self removeDoubles = remove_doubles def smooth(self, factor=0.5, repeat=1, x=True, y=True, z=True ): with self.obj_selected(): bpy.ops.object.modifier_add(type="SMOOTH") m = self.obj.modifiers["Smooth"] m.factor = factor m.iterations = repeat m.use_x = bool(x) m.use_y = bool(y) m.use_z = bool(z) return self def displace(self, strength=1, midlevel=0.5, texture=None, coords_object=None, direction="NORMAL", vertex_group=None ): with self.obj_selected(): bpy.ops.object.modifier_add(type="DISPLACE") m = self.obj.modifiers["Displace"] m.strength = strength m.mid_level = midlevel m.direction = direction if texture and isinstance(texture, str): try: t = bpy.data.textures[texture] m.texture = t except KeyError: bpy.ops.texture.new() t = bpy.data.textures[len(bpy.data.textures)-1] t.name = texture t.type = "CLOUDS" m.texture = t if coords_object: try: m.texture_coords = "OBJECT" m.texture_coords_object = bpy.data.objects[coords_object] except KeyError: print("coords_object not found", coords_object) if vertex_group and isinstance(vertex_group, str): m.vertex_group = vertex_group return self def boolean(self, object, operation="INTERSECT", apply=False, remove=False): target = None with self.obj_selected(): bpy.ops.object.modifier_add(type="BOOLEAN") m = self.obj.modifiers["Boolean"] m.operation = operation target = BpyObj.Find(object) if target: m.object = target.obj #try: # m.object = bpy.data.objects[object] #except KeyError: # print("object for boolean not found", object) if apply: self.apply_modifier("Boolean") if remove and target: target.delete() return self def boolean_diff(self, object, apply=True, remove=True): return self.boolean(object, "DIFFERENCE", apply, remove) def ops_object(self, method, *args, **kwargs): with self.obj_selected(): getattr(bpy.ops.object, method)(*args, **kwargs) return self def shade_flat(self): with self.obj_selected(): bpy.ops.object.shade_flat() return self def shade_smooth(self, auto_smooth=False): if auto_smooth: print("shade smooth use_auto_smooth disabled") with self.obj_selected(): bpy.ops.object.shade_smooth() return self def shade_auto_smooth(self): return self.shade_smooth(auto_smooth=True) shadeSmooth = shade_smooth def auto_smooth(self, angle=30): #print("auto smooth disabled") bpy.ops.object.shade_auto_smooth(use_auto_smooth=True, angle=math.radians(angle)) return self if angle is None: self.obj.data.use_auto_smooth = False else: self.obj.data.use_auto_smooth = True self.obj.data.auto_smooth_angle = math.radians(angle) return self autoSmooth = auto_smooth def subsurface(self): with self.obj_selected(): bpy.ops.object.modifier_add(type="SUBSURF") return self def simpleDeform(self, method="BEND", angle=180, axis="Z", origin=None): with self.obj_selected(): bpy.ops.object.modifier_add(type="SIMPLE_DEFORM") m = self.obj.modifiers["SimpleDeform"] m.deform_method = "BEND" m.deform_axis = axis m.angle = math.radians(angle) if origin: m.origin = bpy.data.objects[origin] return self def decimate_planar(self): with self.obj_selected(): bpy.ops.object.modifier_add(type="DECIMATE") m = self.obj.modifiers["Decimate"] m.decimate_type = "DISSOLVE" return self decimatePlanar = decimate_planar #endregion Modifiers #region Curve functions def draw(self, path:P, cyclic=True, fill=True, tx=0, ty=0, set_origin=True, clear=True, removeOverlap=True) -> "BpyObj": if len(path) > 0: path = path.pen() if removeOverlap: path = path.removeOverlap(use_skia_pathops_draw=False) #print("---"*30) #from pprint import pprint #pprint(path._val.value) amb = path.ambit(tx=tx, ty=ty) origin = amb.x + amb.w/2, amb.y + amb.h/2 path = path.q2c() #czOffset = path.data("centerZeroOffset") splines = [] spline = None value = [] for mv, pts in path._val.value: if mv == "moveTo": p = pts[0] if spline and len(spline) > 0 and spline not in splines: splines.append(spline) spline = [] value.append([p]) spline.append(["BEZIER", "start", [p, p, p]]) elif mv == "lineTo": p = pts[0] value.append([p]) spline.append(["BEZIER", "curve", [p, p, p]]) elif mv == "curveTo": p1, p2, p3 = pts spline[-1][-1][-1] = p1 value.append([p1, p2, p3]) spline.append(["BEZIER", "curve", [p2, p3, p3]]) # elif mv == "qCurveTo": # p1, p2 = pts # start = value[-1][-1] # q1, q2, q3 = raise_quadratic(start, (p1[0], p1[1]), (p2[0], p2[1])) # spline[-1][-1][-1] = q1 # value.append([q1, q2, q3]) # spline.append(["BEZIER", "curve", [q2, q3, q3]]) elif mv == "closePath": if spline and len(spline) > 0 and spline not in splines: splines.append(spline) spline = None spline = None elif mv == "endPath": if spline and len(spline) > 0 and spline not in splines: splines.append(spline) spline = None spline = None else: raise Exception("blender curve unhandled curve type", mv) if spline and len(spline) > 0 and spline not in splines: splines.append(spline) bez = self.obj.data def zvec(pt, z=0): x, y = pt return Vector((x, y, z)) for spline in reversed(bez.splines): # clear existing splines bez.splines.remove(spline) for spline_data in splines: bez.splines.new('BEZIER') spline = bez.splines[-1] spline.use_cyclic_u = cyclic for i, (t, style, pts) in enumerate(spline_data): l, c, r = pts if i > 0: spline.bezier_points.add(1) pt = spline.bezier_points[-1] pt.co = zvec(c) pt.handle_left = zvec(l) pt.handle_right = zvec(r) if fill: bez.dimensions = "2D" bez.fill_mode = "BOTH" if set_origin: self.set_origin(*origin, 0) return self def extrude(self, amount=0.1) -> "BpyObj": self.obj.data.extrude = amount return self def bevel(self, depth=0.02) -> "BpyObj": self.obj.data.bevel_depth = depth return self #endregion ================================================ FILE: packages/coldtype-core/src/coldtype/blender/livepreview.py ================================================ # some code that doesn't really work import bpy import blf import bgl # import skia # from OpenGL import GL # context = skia.GrDirectContext.MakeGL() # backend = skia.GrBackendRenderTarget(1080, 1080, 0, 0, # skia.GrGLFramebufferInfo(0, GL.GL_RGBA8)) # surface = skia.Surface.MakeFromBackendRenderTarget( # context, backend, # skia.kBottomLeft_GrSurfaceOrigin, # skia.kRGBA_8888_ColorType, # skia.ColorSpace.MakeSRGB()) def get_fac(): if bpy.context.space_data.proxy_render_size == 'SCENE': fac = bpy.context.scene.render.resolution_percentage/100 else: fac = 1 return fac def view_zoom_preview(): width = bpy.context.region.width height = bpy.context.region.height rv1 = bpy.context.region.view2d.region_to_view(0,0) rv2 = bpy.context.region.view2d.region_to_view(width-1,height-1) zoom = (1/(width/(rv2[0]-rv1[0])))/get_fac() return zoom class DrawingClass: def __init__(self, msg): self.msg = msg self.handle = bpy.types.SpaceSequenceEditor.draw_handler_add( self.draw_text_callback, (), 'PREVIEW', 'POST_PIXEL') def draw_text_callback(self): #print(">>>", view_zoom_preview()) pt = bpy.context.region.view2d.view_to_region(-540,-540,clip=False) # font_id = 0 # XXX, need to find out how best to get this. # blf.position(font_id, pt[0], pt[1], 0) # blf.size(font_id, 66, 72) # blf.draw(font_id, "%s" % (self.msg)) #GL.glClear(GL.GL_COLOR_BUFFER_BIT) #with surface as context: # context.clear(skia.Color4f(1, 0.3, 0.1, 1)) #surface.flushAndSubmit() def remove_handle(self): bpy.types.SpaceSequenceEditor.draw_handler_remove(self.handle, 'PREVIEW') widgets = {} def register(): widgets["Test"] = DrawingClass("Test2") def unregister(): for key, dc in widgets.items(): dc.remove_handle() if __name__ == "__main__": register() ================================================ FILE: packages/coldtype-core/src/coldtype/blender/panel3d.py ================================================ from coldtype.blender.util import * class Coldtype3DRenderOne(bpy.types.Operator): """Render the current frame via Coldtype with the offline Blender renderer""" bl_idname = "wm.coldtype_3d_render_one" bl_label = "Coldtype 3D Render One" def execute(self, _): print("RENDER ONE") remote("render_index", [bpy.data.scenes[0].frame_current]) return {'FINISHED'} class Coldtype3DRenderFromCurrent(bpy.types.Operator): """Render the current frame via Coldtype with the offline Blender renderer""" bl_idname = "wm.coldtype_3d_render_from_current" bl_label = "Coldtype 3D Render From Current" def execute(self, _): print("RENDER FROM CURRENT") remote("render_after", [bpy.data.scenes[0].frame_current]) return {'FINISHED'} class Coldtype3DRenderAll(bpy.types.Operator): """Render all frames via Coldtype with the offline Blender renderer""" bl_idname = "wm.coldtype_3d_render_all" bl_label = "Coldtype 3D Render All" def execute(self, _): print("RENDER ALL") remote("render_all", [bpy.data.scenes[0].frame_current]) return {'FINISHED'} class Coldtype3DOpenInEditor(bpy.types.Operator): """Open the current Coldtype source file in your configured text editor""" bl_idname = "wm.coldtype_3d_open_in_editor" bl_label = "Coldtype 3D Open-in-editor" def execute(self, _): remote("open_in_editor") return {'FINISHED'} class Coldtype3DShowInFinder(bpy.types.Operator): """Open the current Coldtype source file in the file browser""" bl_idname = "wm.coldtype_3d_show_in_finder" bl_label = "Coldtype 3D Show-in-finder" def execute(self, _): remote("show_in_finder") return {'FINISHED'} class COLDTYPE_3D_PT_Panel(bpy.types.Panel): bl_idname = 'COLDTYPE_3D_PT_panel' bl_label = 'Coldtype 3D' bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = 'Tool' def draw(self, context): layout = self.layout layout.operator(Coldtype3DRenderOne.bl_idname, text="Render One", icon="IMAGE_DATA",) layout.operator(Coldtype3DRenderFromCurrent.bl_idname, text="Render From Current", icon="RENDER_ANIMATION",) layout.operator(Coldtype3DRenderAll.bl_idname, text="Render All", icon="RENDER_ANIMATION",) layout.separator() layout.operator(Coldtype3DOpenInEditor.bl_idname, text="Open in Editor", icon="SCRIPT",) layout.operator(Coldtype3DShowInFinder.bl_idname, text="Show in Finder", icon="FILEBROWSER",) addon_keymaps = [] def register(): bpy.utils.register_class(Coldtype3DRenderOne) bpy.utils.register_class(Coldtype3DRenderFromCurrent) bpy.utils.register_class(Coldtype3DRenderAll) bpy.utils.register_class(Coldtype3DOpenInEditor) bpy.utils.register_class(Coldtype3DShowInFinder) bpy.utils.register_class(COLDTYPE_3D_PT_Panel) def unregister(): for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) addon_keymaps.clear() bpy.utils.unregister_class(COLDTYPE_3D_PT_Panel) def add_3d_panel(): register() ================================================ FILE: packages/coldtype-core/src/coldtype/blender/render.py ================================================ import subprocess, time, sys, json from urllib.parse import urlparse from pathlib import Path from coldtype.osutil import on_windows def prefix_inline_venv(expr): vi = sys.version_info venv_root = Path(sys.executable).parent.parent if on_windows(): venv = (venv_root / "Lib/site-packages").absolute() else: venv = (venv_root / f'lib/python{vi.major}.{vi.minor}/site-packages').absolute() paths = [["venv", venv]] for egg_link in venv.glob("*.egg-link"): paths.append(["egg-link", Path(egg_link).read_text().splitlines()[0]]) for direct_url in venv.glob("**/direct_url.json"): try: info = json.loads(direct_url.read_text()) if info.get("dir_info", {}).get("editable") == True: direct_path = Path(urlparse(info.get("url")).path) if (direct_path / "src").exists(): direct_path = direct_path / "src" paths.append(["direct_url", direct_path]) except Exception as e: print(e) print(">>> could not load direct_url.json for ", direct_url) print(">>>", paths) paths_str = "" for src, path in paths: paths_str += f"sys.path.append(\"{str(Path(path).as_posix())}\");" prefix = f"import sys; from pathlib import Path; {paths_str}" print(prefix) return prefix + " " + expr def blender_launch_livecode(blender_path, file:Path, command_file, additional_args=""): if not file.exists(): file.parent.mkdir(exist_ok=True, parents=True) #call = f"{BLENDER} {file}" print("BLENDER_PATH", blender_path) print(f"Opening blend file: {file}...") cf = Path(command_file).as_posix() args = [ blender_path, file, "--python-expr", prefix_inline_venv(f"from coldtype.blender.watch import watch; watch('{cf}');") ] #if reset_factory: # print("FACTORY RESET") # args.append("--factory-startup") if additional_args: args.extend(additional_args[1:].split(" ")) return subprocess.Popen(args) def blend_frame(blender_path, py_file, blend_file, expr, output_dir, fi): call = [ str(blender_path), "-b", blend_file, "--python-expr", f"{expr}", "-o", f"{output_dir}####.png", "-f", str(fi), ] #call = f"{blender_path} -b \"{blend_file}\" --python-expr \"{expr}\" -o \"{output_dir}####.png\" -f {fi}" print(f"Blending frame {fi}...") print(call) #return #os.system(call) if True: process = subprocess.Popen(call, stdout=subprocess.PIPE, shell=False) log = "" while True: out = process.stdout.read(1).decode("utf-8") log += out if out == "" and process.poll() != None: break if "Error: Python:" in log: print(log) process.kill() process.terminate() break print(log) else: print(subprocess.run(call, stdout=subprocess.PIPE, shell=True)) print(f"/Blended frame {fi}.") def blend_source(blender_path , py_file , blend_file , frame , output_dir , samples=-1 , denoise=False ): """ A facility for telling Blender to render a single frame in a background process """ expr = prefix_inline_venv(f"from coldtype.blender.render import frame_render; frame_render(r'{py_file}', {frame}, {samples}, {denoise})") #print(expr) blend_frame(blender_path, py_file, blend_file, expr, output_dir, frame) def frame_render(file, frame, samples=-1, denoise=False): """ A facility for easy-rendering from within a backgrounded blender """ import bpy from coldtype.renderer.reader import SourceReader from coldtype.blender import walk_to_b3d bpy.data.scenes[0].frame_set(frame) if samples > 0: bpy.data.scenes[0].cycles.samples = samples if denoise: bpy.data.scenes[0].cycles.denoiser = "OPENIMAGEDENOISE" bpy.data.scenes[0].cycles.use_denoising = True sr = SourceReader(file, use_blender=True) for r, res in sr.frame_results(frame, class_filters=[r"^b3d_.*$"]): if hasattr(r, "center"): walk_to_b3d(res, dn=True, renderable=r) sr.unlink() ================================================ FILE: packages/coldtype-core/src/coldtype/blender/timedtext.py ================================================ import json from coldtype.blender.util import * from bpy_extras.io_utils import ImportHelper def text_in_channel(se, c, sort=True): matches = [] for s in se.strips: if hasattr(s, "text") and s.channel == c: matches.append(s) if sort: return sorted(matches, key=lambda s: s.frame_start) return matches def next_neighbor(se, curr): candidates = text_in_channel(se, curr.channel) for c in candidates: if curr.frame_final_end == c.frame_start: return c class TimedTextEditorOperator(bpy.types.Operator): bl_idname = "wm.timed_text_editor_operator" bl_label = "Timed Text Editor" timed_text: bpy.props.StringProperty(name="Text") bl_property = "timed_text" def execute(self, context): self.report({'INFO'}, self.timed_text) se = bpy.data.scenes[0].sequence_editor if hasattr(se.active_strip, "text"): se.active_strip.text = self.timed_text se.active_strip.name = self.timed_text.split(" ")[0] return {'FINISHED'} def invoke(self, context, event): se = bpy.data.scenes[0].sequence_editor self.timed_text = se.active_strip.text wm = context.window_manager return wm.invoke_props_dialog(self) class TimedTextSplitter(bpy.types.Operator): bl_idname = "wm.timed_text_splitter" bl_label = "Timed Text Splitter" def execute(self, context): se = bpy.data.scenes[0].sequence_editor if hasattr(se.active_strip, "text"): next = next_neighbor(se, se.active_strip) if not next: print("ERROR NO NEXT") return {'FINISHED'} curr = se.active_strip txts = curr.text.split(" ") #print(curr.name, next.name, txts) curr.text = txts[0] next.text = " ".join(txts[1:]) curr.name = txts[0] next.name = txts[1] curr.select = False next.select = True se.active_strip = next bpy.ops.sequencer.refresh_all() return {'FINISHED'} def invoke(self, context, event): bpy.ops.sequencer.split(type="SOFT", frame=bpy.data.scenes[0].frame_current, channel=bpy.data.scenes[0].sequence_editor.active_strip.channel, side="RIGHT") bpy.ops.sequencer.refresh_all() return self.execute(context) class TimedTextSelector(bpy.types.Operator): bl_idname = "wm.timed_text_selector" bl_label = "Timed Text Selector" def invoke(self, context, event): se = bpy.data.scenes[0].sequence_editor fc = bpy.data.scenes[0].frame_current if hasattr(se.active_strip, "text"): curr = se.active_strip all = text_in_channel(se, curr.channel) next = None for s in all: if s.frame_start <= fc < s.frame_final_end: next = s if next: curr.select = False next.select = True se.active_strip = next bpy.ops.sequencer.refresh_all() return {'FINISHED'} class TimedTextNewline(bpy.types.Operator): bl_idname = "wm.timed_text_newline" bl_label = "Timed Text Newline" def invoke(self, context, event): se = bpy.data.scenes[0].sequence_editor if hasattr(se.active_strip, "text"): curr = se.active_strip if curr.name.startswith("≈"): curr.name = curr.name[1:] curr.text = curr.text[1:] else: curr.name = "≈" + curr.name curr.text = "≈" + curr.text return {'FINISHED'} class TimedTextReset(bpy.types.Operator): bl_idname = "wm.timed_text_reset" bl_label = "Timed Text Reset" def invoke(self, context, event): se = bpy.data.scenes[0].sequence_editor if hasattr(se.active_strip, "text"): curr = se.active_strip if curr.name.startswith("*"): curr.name = curr.name[1:] curr.text = curr.text[1:] else: curr.name = "*" + curr.name curr.text = "*" + curr.text return {'FINISHED'} class TimedTextRoller(bpy.types.Operator): bl_idname = "wm.timed_text_roller" bl_label = "Timed Text Roller" def invoke(self, context, event): se = bpy.data.scenes[0].sequence_editor fc = bpy.data.scenes[0].frame_current if hasattr(se.active_strip, "text"): curr = se.active_strip all = text_in_channel(se, curr.channel) next = None prev = None for s in all: s.select = False s.select_right_handle = False s.select_left_handle = False if fc == s.frame_start: next = s if fc == s.frame_final_end: prev = s if prev: prev.select = True prev.select_right_handle = True if next: next.select = True next.select_left_handle = True se.active_strip = next bpy.ops.sequencer.refresh_all() return {'FINISHED'} class Coldtype2DImporter(bpy.types.Operator): """Import the current Coldtype animation as a PNG frame sequence in the Blender sequence editor""" bl_idname = "wm.coldtype_2d_importer" bl_label = "Coldtype 2D Importer" def invoke(self, context, event): from coldtype.blender import Action sq = find_sequence() if sq: bpy.ops.sequencer.image_strip_add( directory=str(sq.output_folder) + "/", files=[dict(name=str(p.output_path.name)) for p in sq.passes(Action.RenderAll, None)], relative_path=True, frame_start=0, frame_end=sq.duration-1, channel=2) bpy.ops.sequencer.refresh_all() return {'FINISHED'} class Coldtype2DLivePreviewImporter(bpy.types.Operator): """Import the current Coldtype animation livepreview file as a PNG (that you can display in the image viewer)""" bl_idname = "wm.coldtype_2d_livepreview_importer" bl_label = "Coldtype 2D Live Preview Importer" def invoke(self, context, event): sq = find_sequence() if sq: file = sq.filepath.parent / "renders" / (sq.filepath.stem + "_livepreview.png") if file.exists(): bpy.data.images.load(str(file)) # bpy.ops.sequencer.image_strip_add( # directory=str(sq.output_folder) + "/", # files=[dict(name=str(p.output_path.name)) for p in sq.passes(Action.RenderAll, None)], # relative_path=True, # frame_start=0, # frame_end=sq.duration-1, # channel=2) #bpy.ops.sequencer.refresh_all() return {'FINISHED'} #path = r.filepath.parent / "renders" / (r.filepath.stem + "_livepreview.png") class Coldtype2DSequenceDefaults(bpy.types.Operator): """Some good defaults for editing and rendering a 2D sequence based on PNG images""" bl_idname = "wm.coldtype_2d_sequence_defaults" bl_label = "Coldtype 2D Sequence Defaults" def execute(self, context): context.scene.render.use_compositing = False context.scene.render.use_sequencer = True context.scene.use_audio_scrub = True context.scene.view_settings.view_transform = "Standard" context.screen.use_follow = True return {'FINISHED'} class Coldtype2DRenderOne(bpy.types.Operator): """Render the current frame with the Coldtype renderer""" bl_idname = "wm.coldtype_2d_render_one" bl_label = "Coldtype 2D Render One" def execute(self, _): print("EXTERNAL RENDER ONE") remote("render_index", [bpy.data.scenes[0].frame_current]) return {'FINISHED'} class Coldtype2DRenderWorkarea(bpy.types.Operator): """Render the current workarea with the Coldtype renderer; if not workarea is set, this will render the entire animation""" bl_idname = "wm.coldtype_2d_render_workarea" bl_label = "Coldtype 2D Render Workarea" def execute(self, _): print("RENDER WORKAREA") remote("render_workarea") return {'FINISHED'} class Coldtype2DRenderAll(bpy.types.Operator): """Render the entire animation with the Coldtype renderer""" bl_idname = "wm.coldtype_2d_render_all" bl_label = "Coldtype 2D Render All" def execute(self, _): print("RENDER ALL") remote("render_all") return {'FINISHED'} class Coldtype2DRelease(bpy.types.Operator): """Trigger the release function""" bl_idname = "wm.coldtype_2d_release" bl_label = "Coldtype 2D Release" def execute(self, _): print("RELEASE") remote("release") return {'FINISHED'} def update_json_value(key, value, default=False): jpath = Path(str(Path(bpy.data.filepath)) + ".json") jdata = json.loads(jpath.read_text()) existing = jdata.get(key, default) if callable(value): new_value = value(existing) else: new_value = value jdata[key] = new_value jpath.write_text(json.dumps(jdata)) class Coldtype2DSetWorkarea(bpy.types.Operator): """Set a workarea based on the current frame and the text data in the sequence""" bl_idname = "wm.coldtype_2d_set_workarea" bl_label = "Coldtype 2D Set Workarea" def execute(self, context): sq = find_sequence() if sq: fc = context.scene.frame_current work = sq.t.findWordsWorkarea(fc) if work: update_json_value("workarea_set", True) start, end = work context.scene.frame_start = start context.scene.frame_end = end-1 else: update_json_value("workarea_set", False) context.scene.frame_start = 0 context.scene.frame_end = sq.t.duration-1 return {'FINISHED'} class Coldtype2DUnsetWorkarea(bpy.types.Operator): """Set the workarea to the entire length of the animation""" bl_idname = "wm.coldtype_2d_unset_workarea" bl_label = "Coldtype 2D Unset Workarea" def execute(self, context): sq = find_sequence() if sq: update_json_value("workarea_set", False) context.scene.frame_start = 0 context.scene.frame_end = sq.t.duration-1 return {'FINISHED'} class Coldtype2DOpenInEditor(bpy.types.Operator): """Open the current Coldtype source file in your configured text editor""" bl_idname = "wm.coldtype_2d_open_in_editor" bl_label = "Coldtype 2D Open-in-editor" def execute(self, _): remote("open_in_editor") return {'FINISHED'} class Coldtype2DLivePreviewToggle(bpy.types.Operator): """When livepreviewing is enabled, Coldtype will render a preview image that can be loaded and viewed in realtime in Blender""" bl_idname = "wm.coldtype2d_livepreview_toggle" bl_label = "Coldtype 2D Livepreview Toggle" def execute(self, _): update_json_value("livepreview_disabled", lambda v: not bool(v), False) bpy.ops.sequencer.refresh_all() return {'FINISHED'} class Coldtype2DLoadJSONData(bpy.types.Operator, ImportHelper): """Open file dialog to load json file to sequence""" bl_idname = "wm.coldtype2d_load_json_data" bl_label = "Choose .json file" bl_options = {"REGISTER","UNDO"} filter_glob: bpy.props.StringProperty( default='*.json', options={'HIDDEN'}) def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} def execute(self, context): path = Path(self.filepath) data = json.loads(path.read_text()) for track in data["tracks"]: for clip in track["clips"]: print(clip) se = bpy.data.scenes[0].sequence_editor text = se.strips.new_effect( name=clip["name"], #text=clip["text"], type="TEXT", channel=track["index"], frame_start=int(clip["start"]), frame_end=int(clip["end"]), ) text.text = clip["text"] #text.color = (0, 0.5, 1, 1) return {'FINISHED'} class COLDTYPE_2D_PT_Panel(bpy.types.Panel): bl_idname = 'COLDTYPE_2D_PT_panel' bl_label = 'Coldtype 2D' bl_space_type = 'SEQUENCE_EDITOR' bl_region_type = 'UI' bl_category = 'Tool' def draw(self, context): jpath = str(Path(bpy.data.filepath)) + ".json" jdata = json.loads(Path(jpath).read_text()) layout = self.layout row = layout.row() row.label(text="Livepreview") if jdata.get("livepreview_disabled"): row.operator(Coldtype2DLivePreviewToggle.bl_idname, text="Enable") else: row.operator(Coldtype2DLivePreviewToggle.bl_idname, text="Disable") layout.separator() row = layout.row() row.label(text="Settings") row.operator(Coldtype2DSequenceDefaults.bl_idname, text="Defaults") row.operator(Coldtype2DLoadJSONData.bl_idname, text="Data") row = layout.row() row.label(text="Import") row.operator(Coldtype2DImporter.bl_idname, text="Frames") row.operator(Coldtype2DLivePreviewImporter.bl_idname, text="Preview") layout.separator() row = layout.row() row.label(text="Render") row.operator(Coldtype2DRenderOne.bl_idname, text="", icon="IMAGE_DATA",) row.operator(Coldtype2DRenderAll.bl_idname, text="", icon="RENDER_ANIMATION",) row.operator(Coldtype2DRenderWorkarea.bl_idname, text="", icon="RENDERLAYERS",) row.operator(Coldtype2DRelease.bl_idname, text="", icon="UGLYPACKAGE",) row = layout.row() row.label(text="Workarea") row.operator(Coldtype2DSetWorkarea.bl_idname, text="", icon="STICKY_UVS_VERT",) row.operator(Coldtype2DUnsetWorkarea.bl_idname, text="", icon="STICKY_UVS_LOC",) row = layout.row() row.label(text="Editing") row.operator(TimedTextEditorOperator.bl_idname, text="", icon="OUTLINER_DATA_FONT") row.operator(TimedTextReset.bl_idname, text="", icon="INDIRECT_ONLY_ON") row.operator(TimedTextNewline.bl_idname, text="", icon="OUTLINER_OB_FORCE_FIELD") row.operator(TimedTextSplitter.bl_idname, text="", icon="UV_ISLANDSEL") #layout.label(text="* to start a new text sequence") #layout.label(text="≈ to break a line") #layout.label(text="+ to continue without space") #layout.label(text=". to indicate a style") addon_keymaps = [] def register(): bpy.utils.register_class(TimedTextEditorOperator) bpy.utils.register_class(TimedTextSplitter) bpy.utils.register_class(TimedTextSelector) bpy.utils.register_class(TimedTextNewline) bpy.utils.register_class(TimedTextReset) bpy.utils.register_class(TimedTextRoller) bpy.utils.register_class(Coldtype2DSequenceDefaults) bpy.utils.register_class(Coldtype2DImporter) bpy.utils.register_class(Coldtype2DLivePreviewImporter) bpy.utils.register_class(Coldtype2DRenderOne) bpy.utils.register_class(Coldtype2DRenderWorkarea) bpy.utils.register_class(Coldtype2DRenderAll) bpy.utils.register_class(Coldtype2DRelease) bpy.utils.register_class(Coldtype2DSetWorkarea) bpy.utils.register_class(Coldtype2DUnsetWorkarea) bpy.utils.register_class(Coldtype2DOpenInEditor) bpy.utils.register_class(Coldtype2DLoadJSONData) bpy.utils.register_class(Coldtype2DLivePreviewToggle) bpy.utils.register_class(COLDTYPE_2D_PT_Panel) wm = bpy.context.window_manager kc = wm.keyconfigs.addon if kc: addon_keymaps.append([ km:=kc.keymaps.new(name='Sequencer', space_type='SEQUENCE_EDITOR'), km.keymap_items.new("wm.timed_text_editor_operator", type='T', value='PRESS', shift=True) ]) addon_keymaps.append([ km:=kc.keymaps.new(name='Sequencer', space_type='SEQUENCE_EDITOR'), km.keymap_items.new("wm.timed_text_splitter", type='V', value='PRESS', shift=True) ]) addon_keymaps.append([ km:=kc.keymaps.new(name='Sequencer', space_type='SEQUENCE_EDITOR'), km.keymap_items.new("wm.timed_text_selector", type='D', value='PRESS') ]) addon_keymaps.append([ km:=kc.keymaps.new(name='Sequencer', space_type='SEQUENCE_EDITOR'), km.keymap_items.new("wm.timed_text_newline", type='X', value='PRESS', shift=True) ]) addon_keymaps.append([ km:=kc.keymaps.new(name='Sequencer', space_type='SEQUENCE_EDITOR'), km.keymap_items.new("wm.timed_text_reset", type='R', value='PRESS', shift=True) ]) addon_keymaps.append([ km:=kc.keymaps.new(name='Sequencer', space_type='SEQUENCE_EDITOR'), km.keymap_items.new("wm.timed_text_roller", type='F', value='PRESS') ]) # addon_keymaps.append([ # km:=kc.keymaps.new(name='Sequencer', space_type='SEQUENCE_EDITOR'), # km.keymap_items.new(Coldtype2DImporter.bl_idname, type='I', value='PRESS', shift=True) # ]) # addon_keymaps.append([ # km:=kc.keymaps.new(name='Sequencer', space_type='SEQUENCE_EDITOR'), # km.keymap_items.new(Coldtype2DRenderOne.bl_idname, type='R', value='PRESS') # ]) # addon_keymaps.append([ # km:=kc.keymaps.new(name='Sequencer', space_type='SEQUENCE_EDITOR'), # km.keymap_items.new(Coldtype2DRenderWorkarea.bl_idname, type='R', value='PRESS', shift=True) # ]) # addon_keymaps.append([ # km:=kc.keymaps.new(name='Sequencer', space_type='SEQUENCE_EDITOR'), # km.keymap_items.new(Coldtype2DRenderAll.bl_idname, type='R', value='PRESS', shift=True, oskey=True) # ]) # addon_keymaps.append([ # km:=kc.keymaps.new(name='Sequencer', space_type='SEQUENCE_EDITOR'), # km.keymap_items.new(Coldtype2DOpenInEditor.bl_idname, type='O', value='PRESS') # ]) addon_keymaps.append([ km:=kc.keymaps.new(name='Sequencer', space_type='SEQUENCE_EDITOR'), km.keymap_items.new(Coldtype2DSetWorkarea.bl_idname, type='W', value='PRESS', shift=True) ]) addon_keymaps.append([ km:=kc.keymaps.new(name='Sequencer', space_type='SEQUENCE_EDITOR'), km.keymap_items.new(Coldtype2DUnsetWorkarea.bl_idname, type='W', value='PRESS', shift=True, oskey=True) ]) def unregister(): for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) addon_keymaps.clear() bpy.utils.unregister_class(TimedTextEditorOperator) def add_2d_panel(): register() ================================================ FILE: packages/coldtype-core/src/coldtype/blender/util.py ================================================ import bpy, json from pathlib import Path def find_sequence(): from coldtype.blender import b3d_sequencer, b3d_animation rs = bpy.app.driver_namespace.get("_coldtypes", []) sq = None for r in rs: if isinstance(r, b3d_sequencer) or isinstance(r, b3d_animation): sq = r return sq def remote(command, args=None, sq=None): #print("REMOTE", command, args, sq) if sq is None: sq = find_sequence() input_command_file = bpy.app.driver_namespace["_coldtype_command_input_file"] (Path(input_command_file) .expanduser() .write_text(json.dumps(dict( action=command if isinstance(command, str) else command.value, args=args, filepath=str(sq.filepath))))) return sq ================================================ FILE: packages/coldtype-core/src/coldtype/blender/watch.py ================================================ import bpy, traceback, json, shutil from pathlib import Path from collections import defaultdict from coldtype.renderable.animation import animation from coldtype.renderer.reader import SourceReader from coldtype.blender import B3DPlayback, b3d_animation, b3d_renderable, b3d_runnable, b3d_sequencer, walk_to_b3d from coldtype.blender.timedtext import add_2d_panel from coldtype.blender.panel3d import add_3d_panel from coldtype.blender.util import remote def persist_sequence(last_persisted): channels = defaultdict(lambda: []) scene = bpy.data.scenes[0] for s in scene.sequence_editor.strips: channels[s.channel].append(s) tracks = [] for c, clips in channels.items(): track = dict(index=c) _clips = [] for clip in clips: if hasattr(clip, "text"): _clips.append(dict( name=clip.name, text=clip.text, start=clip.frame_start, end=clip.frame_final_end)) track["clips"] = sorted(_clips, key=lambda c: c["start"]) if len(_clips) > 0: tracks.append(track) if len(tracks) == 0: return None jpath = str(Path(bpy.data.filepath)) + ".json" jdata = json.loads(Path(jpath).read_text()) #print(">>>>>>>>>>>>>>>>", scene.render.fps, scene.render.fps_base) out = dict( start=scene.frame_start, end=scene.frame_end, #fps=scene.render.fps / scene.render.fps_base, workarea_set=jdata.get("workarea_set", False), livepreview_disabled=jdata.get("livepreview_disabled"), #current_frame=scene.frame_current, tracks=tracks) if out == last_persisted: return out else: #print("NEW CHANGES") Path(jpath).write_text(json.dumps(out, indent=4)) autosave = False for r in bpy.app.driver_namespace.get("_coldtypes", []): if hasattr(r, "reread_timeline"): r.reread_timeline() if hasattr(r, "autosave") and r.autosave: autosave = True if autosave: bpy.ops.wm.save_mainfile() return out def render_as_image(r, res): from coldtype.pens.skiapen import SkiaPen path = r.filepath.parent / "renders" / (r.filepath.stem + "_livepreview.png") if isinstance(res, Path) or isinstance(res, str) or False: prp = Path(str(res)) if prp.exists(): shutil.copy(str(prp), str(path)) else: _ = SkiaPen.Precompose(res, r.rect, disk=str(path), scale=r.live_preview_scale) return path # if r.name not in bpy.data.images.keys(): # bpy.data.images.new(r.name, width=r.rect.w, height=r.rect.h, alpha=True, float_buffer=True) # img = bpy.data.images[r.name] # img.pixels = pimg.toarray(colorType=skia.ColorType.kRGBA_F32_ColorType).ravel() # original idea: https://blender.stackexchange.com/questions/15670/send-instructions-to-blender-from-external-application def display_image_in_blender(img_path): try: if img_path.name in bpy.data.images: bpy.data.images[img_path.name].reload() else: bpy.data.images.load(str(img_path)) except RuntimeError: print("> failed to load livepreview") class ColdtypeWatchingOperator(bpy.types.Operator): bl_idname = "wm.coldtype_watching_operator" bl_label = "Coldtype Watching Operator" _timer = None file = None sr = None current_frame = -1 persisted = None _delayed_runnables = [] _should_start_playing = False def render_current_frame(self, statics=False): delayed_runnables = [] out = [] animation_found = False frame = self.current_frame playback = B3DPlayback.AlwaysStop def display_image(r, result): lp_path = render_as_image(r, result) display_image_in_blender(lp_path) candidates = sorted(self.candidates, key=lambda x: isinstance(x, b3d_runnable), reverse=True) force_refresh = False for r in candidates: if isinstance(r, b3d_runnable): if statics: playback = r.playback if hasattr(r, "force_refresh") and r.force_refresh: force_refresh = True if statics and playback != B3DPlayback.KeepPlaying: if bpy.context.screen.is_animation_playing: bpy.ops.screen.animation_play() # stop it for r in candidates: if isinstance(r, b3d_runnable): if r.once: if statics: if r.delay: delayed_runnables.append(r) else: r.run() else: if r.delay: delayed_runnables.append(r) else: r.run() else: out.append(r) ps = r.passes(None, None, indices=[frame]) def run_passes(): return r.run_normal(ps[0], renderer_state=None) if isinstance(r, b3d_sequencer) and r.renderer == "skia": animation_found = True if r.live_preview: result = run_passes() display_image(r, result) elif isinstance(r, b3d_animation): if r.bake: if statics: walk_to_b3d(r.baked_frames(), renderable=r) if r.reset_to_zero: bpy.data.scenes[0].frame_set(0) else: animation_found = True result = run_passes() if result and (result.depth() > 0 or not result.empty()): if r.renderer == "b3d": walk_to_b3d(result, renderable=r) else: raise Exception("r.renderer not supported", r.renderer) if r.reset_to_zero: bpy.data.scenes[0].frame_set(0) elif isinstance(r, b3d_renderable): # coolio if statics: result = run_passes() walk_to_b3d(result, renderable=r) if r.reset_to_zero: bpy.data.scenes[0].frame_set(0) #if not animation_found: # bpy.data.scenes[0].frame_set(0) if statics: bpy.app.driver_namespace["_coldtypes"] = out if force_refresh: for area in bpy.context.screen.areas: if area.type == 'VIEW_3D': for space in area.spaces: if space.type == 'VIEW_3D': current = space.shading.type if current == "RENDERED": space.shading.type = 'WIREFRAME' space.shading.type = 'RENDERED' for o in out: if hasattr(o, "post_run") and o.post_run: o.post_run() if statics: self._delayed_runnables = delayed_runnables # for dr in self._delayed_runnables: # dr.run() # self._delayed_runnables = [] if playback == B3DPlayback.AlwaysPlay: bpy.ops.screen.animation_play() return animation_found def reimport(self, arg, inputs): inputs_dict = {} try: self.sr = SourceReader(arg, use_blender=True, inputs=inputs) self.sr.unlink() self.candidates = self.sr.renderables() #bpy.data.scenes[0].frame_start = 0 from coldtype.tool import parse_inputs inputs_dict = parse_inputs(self.sr.inputs, { "quit": [False, bool] }, positional=False, ui=None) bpy.app.handlers.frame_change_pre.clear() self.current_frame = bpy.context.scene.frame_current animation_found = self.render_current_frame(statics=True) if not animation_found: print(f"ran {arg}") if inputs_dict.get("quit"): bpy.ops.wm.quit_blender() return def _frame_update_handler(scene): if scene.frame_current != self.current_frame: self.current_frame = scene.frame_current self.render_current_frame(statics=False) bpy.app.handlers.frame_change_pre.append(_frame_update_handler) except Exception as e: self.current_frame = -1 bpy.app.handlers.frame_change_pre.clear() stack = traceback.format_exc() print("---"*10) print(stack) print(f"ran {arg}") if inputs_dict.get("quit"): bpy.ops.wm.quit_blender() def modal(self, context, event): if event.type == 'TIMER': for dr in self._delayed_runnables: dr.run() self._delayed_runnables = [] # if self._should_start_playing: # if not bpy.context.screen.is_animation_playing: # bpy.ops.screen.animation_play() # self._should_start_playing = False if bpy.app.driver_namespace.get("_coldtype_needs_rerender", False): bpy.app.driver_namespace["_coldtype_needs_rerender"] = False self.render_current_frame(statics=False) if not bpy.context.screen.is_animation_playing: self.persisted = persist_sequence(self.persisted) if not self.file.exists(): return {'PASS_THROUGH'} for line in self.file.read_text().splitlines(): #print(line) try: line = line.rstrip("\n") start, kwargs = [t.strip() for t in line.split(";")] kwargs = eval(kwargs) cmd, arg = start.split(",") if cmd == "import": self.reimport(arg, kwargs) elif cmd == "play_preview": bpy.ops.screen.animation_play() elif cmd == "frame_offset": bpy.ops.screen.frame_offset(delta=int(arg)) elif cmd == "refresh_sequencer": bpy.ops.sequencer.refresh_all() for k, v in bpy.data.images.items(): print("RELOAD", k, v) v.reload() elif cmd == "refresh_sequencer_and_image": bpy.ops.sequencer.refresh_all() for k, v in bpy.data.images.items(): if "_last_render.png" in k: v.reload() elif cmd == 'cancel': self.cancel(context) else: print('unknown request=%s arg=%s' % (cmd,arg)) except Exception as e: print(">>>>>>>>>>>>>>>>>", line) print("Failed to read command file:", e) if self.file.exists(): self.file.unlink() return {'PASS_THROUGH'} def execute(self, context): ccf = bpy.app.driver_namespace.get("_coldtype_command_output_file") self.file = Path(ccf) wm = context.window_manager self._timer = wm.event_timer_add(0.25, window=context.window) wm.modal_handler_add(self) return {'RUNNING_MODAL'} def cancel(self, context): wm = context.window_manager wm.event_timer_remove(self._timer) print('timer removed') def register_watcher(): bpy.utils.register_class(ColdtypeWatchingOperator) def unregister_watcher(): bpy.utils.unregister_class(ColdtypeWatchingOperator) def watch(command_file): input_file = command_file.replace(".txt", "_input.json") bpy.app.driver_namespace["_coldtype_command_output_file"] = command_file bpy.app.driver_namespace["_coldtype_command_input_file"] = input_file register_watcher() bpy.ops.wm.coldtype_watching_operator() add_3d_panel() add_2d_panel() print("...blender waiting for coldtype...") ================================================ FILE: packages/coldtype-core/src/coldtype/capture/__init__.py ================================================ try: import cv2 except ImportError: raise Exception("pip install opencv-python") try: import numpy as np except ImportError: pass from coldtype.img.skiaimage import skia, SkiaImage def read_frame(cam): _, frame = cam.read() r_channel, g_channel, b_channel = cv2.split(frame) alpha_channel = np.ones(b_channel.shape, dtype=b_channel.dtype) * 255 img_BGRA = cv2.merge((b_channel, g_channel, r_channel, alpha_channel)) return SkiaImage(skia.Image.fromarray(img_BGRA)) ================================================ FILE: packages/coldtype-core/src/coldtype/color/__init__.py ================================================ from random import random from coldtype.color.html import NAMED_COLORS from coldtype.interpolation import norm try: import skia except ImportError: skia = None try: from fontTools.ttLib.tables.C_P_A_L_ import Color as FTCPALColor except ImportError: FTCPALColor = None def norm(value, start, stop): return start + (stop-start) * value def lerp(start, stop, amt): return float(amt-start) / float(stop-start) # inspired by https://github.com/xav/Grapefruit/blob/master/grapefruit.py # but more shorthand-oriented def hue2rgb(n1, n2=None, h=None): h %= 6.0 if h < 1.0: return n1 + ((n2-n1) * h) if h < 3.0: return n2 if h < 4.0: return n1 + ((n2-n1) * (4.0 - h)) return n1 def hsl_to_rgb(h, s=0, l=0): if s == 0: return (l, l, l) if l < 0.5: n2 = l * (1.0 + s) else: n2 = l+s - (l*s) n1 = (2.0 * l) - n2 h /= 60.0 r = hue2rgb(n1, n2, h + 2) g = hue2rgb(n1, n2, h) b = hue2rgb(n1, n2, h - 2) return (r, g, b) def rgb_to_hsl(r, g=None, b=None): minVal = min(r, g, b) maxVal = max(r, g, b) l = (maxVal + minVal) / 2.0 if minVal == maxVal: return (0.0, 0.0, l) d = maxVal - minVal if l < 0.5: s = d / (maxVal + minVal) else: s = d / (2.0 - maxVal - minVal) dr, dg, db = [(maxVal-val) / d for val in (r, g, b)] if r == maxVal: h = db - dg elif g == maxVal: h = 2.0 + dr - db else: h = 4.0 + dg - dr h = (h*60.0) % 360.0 return (h, s, l) class Color(): def __init__(self, *values): r, g, b = [float(v) for v in values[:3]] self.r = float(values[0]) self.g = float(values[1]) self.b = float(values[2]) if len(values) > 3: self.a = float(values[3]) else: self.a = 1 h, s, l = rgb_to_hsl(r, g, b) self.h = h self.hp = h/360 self.s = s self.l = l self.html = self.to_html() self.src = None def __eq__(self, other): if isinstance(other, Color): return self.r == other.r and self.g == other.g and self.b == other.b and self.a == other.a else: return False def __hash__(self): return hash((self.r, self.g, self.b, self.a)) def to_code(self): if self.a == 1: if self.s == 0: return f"bw({self.l})" elif self.a < 1: if self.s == 0: return f"bw({self.l}, {self.a})" return f"hsl({round(self.h/360.0, 2)}, {round(self.s, 2)}, {round(self.l, 2)}, {round(self.a, 2)})" def with_alpha(self, alpha): return Color(self.r, self.g, self.b, alpha) a = with_alpha def ints(self): return [self.r*255, self.g*255, self.b*255, self.a] def __getitem__(self, index): return [self.r, self.g, self.b, self.a][index] def from_rgb(r, g, b, a=1): return Color(r, g, b, a) def from_html(html, a=1): html = html.strip().lower() if html == "r": return Color(1,0,0) elif html == "g": return Color(0,1,0) elif html == "b": return Color(0,0,1) elif html[0] == '#': html = html[1:] elif html in NAMED_COLORS: html = NAMED_COLORS[html][1:] if len(html) == 6: rgb = html[:2], html[2:4], html[4:] elif len(html) == 3: rgb = ['%c%c' % (v, v) for v in html] else: raise ValueError("input #%s is not in #RRGGBB format" % html) return Color(*[(int(n, 16) / 255.0) for n in rgb], a) def to_html(self): return '#%02x%02x%02x' % tuple((min(round(v*255), 255) for v in (self.r, self.g, self.b))) def lighter(self, level): return Color.from_hsl(self.h, self.s, min(self.l + level, 1), self.a) def desaturate(self, level): return Color.from_hsl(self.h, max(self.s - level, 0), self.l, self.a) def saturate(self, level): return Color.from_hsl(self.h, min(self.s + level, 1), self.l, self.a) def darker(self, level): return Color.from_hsl(self.h, self.s, max(self.l - level, 0), self.a) def adjust(self, level): return self.lighter(level) adj = adjust def invert(self): newR = 1.0 - self.r newG = 1.0 - self.g newB = 1.0 - self.b return Color.from_rgb(newR, newG, newB, self.a) def from_hsl(h, s, l, a=1): r, g, b = hsl_to_rgb(h, s, l) c = Color(r, g, b, a) c.src = "from_hsl" return c def rgba(self): return self.r, self.g, self.b, self.a def interp(self, v, other): return self.hsl_interp(v, other) def hsl_interp(self, v, other): return hsl(norm(v, self.h, other.h)/360.0, norm(v, self.s, other.s), norm(v, self.l, other.l), norm(v, self.a, other.a)) def rgb_interp(self, v, other): return rgb(norm(v, self.r, other.r), norm(v, self.g, other.g), norm(v, self.b, other.b), norm(v, self.a, other.a)) # def __str__(self): # return "".format(self.r, self.g, self.b, self.a) def __repr__(self): return "".format(self.r, self.g, self.b, self.h/360, self.s, self.l, self.a) def skia(self): if skia: return skia.Color4f(self.r, self.g, self.b, self.a) else: raise Exception("Skia installation not found") class Theme(): def __init__(self, default=None, **kwargs): self.colors = {} if default is not None: self.colors["default"] = normalize_color(default) for k, v in kwargs.items(): self.colors[k] = normalize_color(v) def __getitem__(self, index): if isinstance(index, int): return list(self.colors.values())[index] else: return self.colors.get(index) def __setitem__(self, key, value): self.colors[key] = value def get(self, key, default=None): return self.colors.get(key, self.colors.get("default", default)) def __len__(self): return len(self.colors.values()) def __repr__(self): return f"Theme(colors:{len(self.colors.values())})" def with_alpha(self, a): if isinstance(a, float) or isinstance(a, int): return Theme(**{k:v.with_alpha(a) for k,v in self.colors.items()}) else: return Theme(**{k:v.with_alpha(a[i]) for i,(k,v) in enumerate(self.colors.items())}) def adjust(self, a): if isinstance(a, float) or isinstance(a, int): return Theme(**{k:v.lighter(a) for k,v in self.colors.items()}) else: return Theme(**{k:v.lighter(a[i]) for i,(k,v) in enumerate(self.colors.items())}) a = with_alpha adj = adjust def lighten_max(color, maxLightness=0.55): return Color.from_hsl(color.h, color.s, max(maxLightness, color.l)) def color_var(*rgba): c = [random() if x == -1 or x == "random" or x == "rand" or x == "R" else x for x in rgba] if len(c) == 1: return Color.from_rgb(c[0], c[0], c[0]) elif len(c) == 2: return Color.from_rgb(c[0], c[0], c[0], c[1]) elif len(c) == 3: return Color.from_rgb(c[0], c[1], c[2]) elif len(c) == 4: return Color.from_rgb(c[0], c[1], c[2], c[3]) def hex_to_tuple(h): return tuple([c/255 for c in (h.r, h.g, h.b, h.a)]) def find_random(v): from random import randint if isinstance(v, str): if v == "random" or v == "r": return random() elif v.startswith("r"): v = v[1:] if "-" in v: limits = [float(x.strip()) for x in v.split("-")] return random() * (limits[1]-limits[0]) + limits[0] elif "," in v: options = [float(x.strip()) for x in v.split(",")] return options[randint(0, len(options))] try: return float(v) except: return v def normalize_color(v): if v == -1: return Color.from_rgb(0,0,0,0) if v is None: return Color.from_rgb(0,0,0,0) elif isinstance(v, Color): return v try: if isinstance(v[0], Color): return v[0] except: pass if isinstance(v, Gradient): return v elif isinstance(v, float) or isinstance(v, int): return Color.from_rgb(v, v, v) elif FTCPALColor and isinstance(v, FTCPALColor): return Color.from_rgb(v.red/255, v.green/255, v.blue/255, v.alpha/255) elif isinstance(v, str): if v == "random" or v == -1: return Color.from_rgb(random(), random(), random()) elif v == "none": return Color.from_rgb(0,0,0,0) else: return Color.from_html(v) else: if len(v) == 1: if v[0] == -1: return Color.from_rgb(0,0,0,0) if v[0] == "random": return Color.from_rgb(random(), random(), random(), 1) if v[0] == None: return Color.from_rgb(0,0,0,0) elif isinstance(v[0], str): return Color.from_html(v[0]) elif isinstance(v[0], Gradient): return v[0] else: try: iter(v[0]) return normalize_color(v[0]) except TypeError: return Color.from_rgb(v[0], v[0], v[0]) elif len(v) == 2: if v[0] == "random" or v[0] == -1: return Color.from_rgb(random(), random(), random(), float(v[1])) elif isinstance(v[0], str): return Color.from_html(v[0]).with_alpha(v[1]) else: c = Color.from_rgb(v[0], v[0], v[0], v[1]) return c else: if isinstance(v[0], complex): vs = [find_random(x) for x in v] return Color.from_hsl(v[0].imag*360, *vs[1:]) if isinstance(v[0], str) and v[0].startswith("h"): v = list(v) v[0] = v[0][1:] vs = [find_random(x) for x in v] return Color.from_hsl(vs[0]*360, *vs[1:]) else: vs = [random() if _v == "random" else _v for _v in v] return Color.from_rgb(*vs) def hsl(h, s=0.5, l=0.5, a=1): return Color.from_hsl(h*360, s, l, a) def hsl_(hsla): return hsl(*hsla) def hsl360(h, s=50, l=50, a=1): return hsl(h/360, s/100, l/100, a) def rgb(r, g, b, a=1): return Color.from_rgb(r, g, b, a) def rgb_(rgba): return hsl(*rgba) def rgb255(r, g, b, a=1): return rgb(r/255, g/255, b/255, a) def bw(c, a=1): return Color.from_rgb(c, c, c, a) class Gradient(): def __init__(self, *stops): self.stops = [] for c, p in stops: self.addStop(c, p) def addStop(self, color, point): self.stops.append([normalize_color(color), point]) def Vertical(rect, a, b): return Gradient([a, rect.point("N")], [b, rect.point("S")]) def Horizontal(rect, a, b): return Gradient([a, rect.point("W")], [b, rect.point("E")]) def Random(rect, opacity=0.5): return Gradient([("random", opacity), rect.point("SE")], [("random", opacity), rect.point("NW")]) V = Vertical H = Horizontal R = Random ================================================ FILE: packages/coldtype-core/src/coldtype/color/html.py ================================================ NAMED_COLORS = { 'aliceblue': '#f0f8ff', 'antiquewhite': '#faebd7', 'aqua': '#00ffff', 'aquamarine': '#7fffd4', 'azure': '#f0ffff', 'beige': '#f5f5dc', 'bisque': '#ffe4c4', 'black': '#000000', 'blanchedalmond': '#ffebcd', 'blue': '#0000ff', 'blueviolet': '#8a2be2', 'brown': '#a52a2a', 'burlywood': '#deb887', 'cadetblue': '#5f9ea0', 'chartreuse': '#7fff00', 'chocolate': '#d2691e', 'coral': '#ff7f50', 'cornflowerblue': '#6495ed', 'cornsilk': '#fff8dc', 'crimson': '#dc143c', 'cyan': '#00ffff', 'darkblue': '#00008b', 'darkcyan': '#008b8b', 'darkgoldenrod': '#b8860b', 'darkgray': '#a9a9a9', 'darkgrey': '#a9a9a9', 'darkgreen': '#006400', 'darkkhaki': '#bdb76b', 'darkmagenta': '#8b008b', 'darkolivegreen': '#556b2f', 'darkorange': '#ff8c00', 'darkorchid': '#9932cc', 'darkred': '#8b0000', 'darksalmon': '#e9967a', 'darkseagreen': '#8fbc8f', 'darkslateblue': '#483d8b', 'darkslategray': '#2f4f4f', 'darkslategrey': '#2f4f4f', 'darkturquoise': '#00ced1', 'darkviolet': '#9400d3', 'deeppink': '#ff1493', 'deepskyblue': '#00bfff', 'dimgray': '#696969', 'dimgrey': '#696969', 'dodgerblue': '#1e90ff', 'firebrick': '#b22222', 'floralwhite': '#fffaf0', 'forestgreen': '#228b22', 'fuchsia': '#ff00ff', 'gainsboro': '#dcdcdc', 'ghostwhite': '#f8f8ff', 'gold': '#ffd700', 'goldenrod': '#daa520', 'gray': '#808080', 'grey': '#808080', 'green': '#008000', 'greenyellow': '#adff2f', 'honeydew': '#f0fff0', 'hotpink': '#ff69b4', 'indianred': '#cd5c5c', 'indigo': '#4b0082', 'ivory': '#fffff0', 'khaki': '#f0e68c', 'lavender': '#e6e6fa', 'lavenderblush': '#fff0f5', 'lawngreen': '#7cfc00', 'lemonchiffon': '#fffacd', 'lightblue': '#add8e6', 'lightcoral': '#f08080', 'lightcyan': '#e0ffff', 'lightgoldenrodyellow': '#fafad2', 'lightgreen': '#90ee90', 'lightgray': '#d3d3d3', 'lightgrey': '#d3d3d3', 'lightpink': '#ffb6c1', 'lightsalmon': '#ffa07a', 'lightseagreen': '#20b2aa', 'lightskyblue': '#87cefa', 'lightslategray': '#778899', 'lightslategrey': '#778899', 'lightsteelblue': '#b0c4de', 'lightyellow': '#ffffe0', 'lime': '#00ff00', 'limegreen': '#32cd32', 'linen': '#faf0e6', 'magenta': '#ff00ff', 'maroon': '#800000', 'mediumaquamarine': '#66cdaa', 'mediumblue': '#0000cd', 'mediumorchid': '#ba55d3', 'mediumpurple': '#9370db', 'mediumseagreen': '#3cb371', 'mediumslateblue': '#7b68ee', 'mediumspringgreen': '#00fa9a', 'mediumturquoise': '#48d1cc', 'mediumvioletred': '#c71585', 'midnightblue': '#191970', 'mintcream': '#f5fffa', 'mistyrose': '#ffe4e1', 'moccasin': '#ffe4b5', 'navajowhite': '#ffdead', 'navy': '#000080', 'oldlace': '#fdf5e6', 'olive': '#808000', 'olivedrab': '#6b8e23', 'orange': '#ffa500', 'orangered': '#ff4500', 'orchid': '#da70d6', 'palegoldenrod': '#eee8aa', 'palegreen': '#98fb98', 'paleturquoise': '#afeeee', 'palevioletred': '#db7093', 'papayawhip': '#ffefd5', 'peachpuff': '#ffdab9', 'peru': '#cd853f', 'pink': '#ffc0cb', 'plum': '#dda0dd', 'powderblue': '#b0e0e6', 'purple': '#800080', 'red': '#ff0000', 'rosybrown': '#bc8f8f', 'royalblue': '#4169e1', 'saddlebrown': '#8b4513', 'salmon': '#fa8072', 'sandybrown': '#f4a460', 'seagreen': '#2e8b57', 'seashell': '#fff5ee', 'sienna': '#a0522d', 'silver': '#c0c0c0', 'skyblue': '#87ceeb', 'slateblue': '#6a5acd', 'slategray': '#708090', 'slategrey': '#708090', 'snow': '#fffafa', 'springgreen': '#00ff7f', 'steelblue': '#4682b4', 'tan': '#d2b48c', 'teal': '#008080', 'thistle': '#d8bfd8', 'tomato': '#ff6347', 'turquoise': '#40e0d0', 'violet': '#ee82ee', 'wheat': '#f5deb3', 'white': '#ffffff', 'whitesmoke': '#f5f5f5', 'yellow': '#ffff00', 'yellowgreen': '#9acd32' } ================================================ FILE: packages/coldtype-core/src/coldtype/css.py ================================================ from coldtype.geometry.rect import Rect from coldtype.color import hsl from coldtype.runon.path import P def cubicBezier(x1, y1, x2, y2): p = P() p.moveTo((0, 0)) p.curveTo( (x1*1000, y1*1000), (x2*1000, y2*1000), (1000, 1000)) p.endPath() p.data(frame=Rect(1000, 1000)) return p.fssw(-1, hsl(0.6), 2) ================================================ FILE: packages/coldtype-core/src/coldtype/demo/blank.py ================================================ from coldtype import * co = Font.Cacheable(__sibling__("ColdtypeObviously-VF.ttf")) mu = Font.Cacheable(__sibling__("MutatorSans.ttf")) @renderable((500, 500), bg=hsl(0.75, 1, 0.7)) def render(r): circle = (P().oval(r.inset(70)).reverse()) return P( (StSt("No renderables found in source file ... ".upper(), mu, 50, wdth=0.19, wght=0.5) .f(0, 0.5) .distribute_on_path(circle.rotate(-150))), (StSt("COLD\nTYPE", co, 130, r=1, wdth=0.5, tu=50) .f(hsl(0.07, 1, 0.8)) .align(r))) ================================================ FILE: packages/coldtype-core/src/coldtype/demo/boiler.py ================================================ from coldtype import * @animation() def scratch(f:Frame): return (P() .rect(f.a.r.inset(150)) .rotate(f.e("ceio", 1, rng=(-0.5, 0.5))) .f(hsl(0.65))) ================================================ FILE: packages/coldtype-core/src/coldtype/demo/boiler_renderable.py ================================================ from coldtype import * @renderable() def scratch(r:Rect): return (P() .rect(r.inset(150))) ================================================ FILE: packages/coldtype-core/src/coldtype/demo/demo.py ================================================ from coldtype import * co = Font.ColdtypeObviously() @animation((800, 200), timeline=60, bg=1) def demo(f): return (Glyphwise("COLDTYPE", lambda x: Style(co, 150 , wdth=f.adj(-x.i*15).e("eeo", 1))) .align(f.a.r) .f(0)) ================================================ FILE: packages/coldtype-core/src/coldtype/demo/demoblender.py ================================================ from coldtype import * from coldtype.blender import * @b3d_runnable(playback=B3DPlayback.KeepPlaying) def prerun(bw:BpyWorld): bw.delete_previous(materials=False) @b3d_animation(timeline=60, center=(0, 1), upright=1) def varfont2(f): return (P( Glyphwise("COLD\nTYPE", lambda g: Style(Font.ColdtypeObviously(), 375 , wdth=f.adj(-g.i*5).e("seio", 1, rng=(0.98, 0)))) .xalign(f.a.r) .track(50, v=1) .align(f.a.r) .mapv(lambda i, p: p .ch(b3d(lambda bp: bp .extrude(f.adj(-i*5) .e("seio", 1, rng=(0.1, 1.75)))))))) ================================================ FILE: packages/coldtype-core/src/coldtype/demo/docstrings.py ================================================ from coldtype import * from coldtype.tool import parse_inputs from runpy import run_path import inspect if __as_config__: raise ColdtypeCeaseConfigException() args = parse_inputs(__inputs__, dict( path=[None, Path, "Must provide file path"])) res = run_path(args["path"]) _renderables = [] for k, v in res.items(): docstring = inspect.getdoc(v) if docstring and "---\n@example" in docstring: _renderables.append(exec("from coldtype import *;from coldtype.renderable.renderable import example\n" + docstring.split("---")[-1])) @renderable((800, 20), watch=[args["path"]]) def updater(r): return None ================================================ FILE: packages/coldtype-core/src/coldtype/demo/gifski.py ================================================ from coldtype import * from coldtype.tool import parse_inputs from coldtype.img.skiaimage import SkiaImage from coldtype.renderable.animation import raw_gifski if __as_config__: raise ColdtypeCeaseConfigException() args = parse_inputs(__inputs__, dict( path=[None, str, "Must provide path to frames"], fps=[18, float], output_folder=[None, str], name=[None, str], )) template_path = Path(args["path"]).absolute() if template_path.exists() and template_path.is_dir(): template = str(template_path) + "/" + "{:04d}.png" else: template = str(template_path) + "{:04d}.png" img0_path = ººsiblingºº(template.format(0)) print(img0_path) img0 = SkiaImage(img0_path) name = args["name"] if name is None: name = img0_path.parent.stem count = len(list(img0_path.parent.glob("*.png"))) output_folder = args["output_folder"] if output_folder is None: output_folder = img0_path.parent.parent output_folder = Path(output_folder) output_folder.mkdir(exist_ok=True, parents=True) #print(args) print(">", name) @animation(img0.rect(), tl=Timeline(count, fps=args["fps"]), name=name) def gifmaker(f): return (SkiaImage(ººsiblingºº(template.format(f.i)))) @renderable(Rect(img0.rect().w, 70), bg=None) def instructions(r): return (StSt(str(args["fps"]) + " fps / [r] (release) for gif", Font.JBMono(), 32, wght=1) .align(r) .f(0)) def release(_): raw_frames = [] for fi in range(gifmaker.timeline.start, gifmaker.timeline.end): raw_frames.append(template.format(fi)) if not Path(raw_frames[-1]).exists(): raise Exception("frame does not exist", raw_frames[-1]) raw_gifski(gifmaker.rect.w, gifmaker.timeline.fps, raw_frames, output_folder / (name + ".gif"), open=True) ================================================ FILE: packages/coldtype-core/src/coldtype/demo/glfw34.py ================================================ from coldtype import * import zipfile, requests, io, subprocess, sys, shutil file = Path(ººFILEºº) zip_filename = "glfw-3.4.bin.MACOS" zip_src = f"https://github.com/glfw/glfw/releases/download/3.4/{zip_filename}.zip" zip_dst = file.parent @renderable() def glfw34(r): if not (zip_dst / zip_filename).exists(): print("downloading...") r = requests.get(zip_src) z = zipfile.ZipFile(io.BytesIO(r.content)) z.extractall(zip_dst) glfw_src = zip_dst / zip_filename dylib_src = glfw_src / "lib-universal/libglfw.3.dylib" print(dylib_src) if dylib_src.exists(): print("yes") vi = sys.version_info dylib_dst = Path(sys.executable).parent.parent / f"lib/python{vi.major}.{vi.minor}/site-packages/glfw/libglfw.3.dylib" if not dylib_dst.exists(): print("could not find glfw installation") else: shutil.copyfile(dylib_src, dylib_dst) # p1 = subprocess.Popen(["which", "python"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # out, err = p1.communicate() # which_python = out.strip() # p2 = subprocess.Popen([which_python, "-c", "import sysconfig;print(sysconfig.get_config_var('installed_base'))"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # out, err = p2.communicate() # installed_base = out.strip() # if isinstance(installed_base, bytes): # installed_base = installed_base.decode("utf-8") # print(which_python, installed_base) return None ================================================ FILE: packages/coldtype-core/src/coldtype/drawbot.py ================================================ import contextlib import drawBot as db from coldtype.runon.path import P from coldtype.pens.drawbotpen import DrawBotPen from coldtype.geometry import Point, Line, Rect from coldtype.text.reader import StyledString, Style, Font from coldtype.text.composer import StSt from coldtype.color import hsl, bw from coldtype.timing import Frame from pathlib import Path from coldtype.renderable.renderable import renderable, Action from coldtype.renderable.animation import animation, RenderPass # from coldtype.osutil import in_notebook # _in_notebook = in_notebook() # if _in_notebook: # from coldtype.notebook import animation, renderable try: import drawBot as db import AppKit except ImportError: print("No DrawBot installed! `pip install git+https://github.com/typemytype/drawbot`") db = None class drawbot_renderable(renderable): def __init__(self, rect=(1080, 1080), scale=1, **kwargs): if not db: raise Exception("DrawBot not installed!") super().__init__(rect=Rect(rect).scale(scale), rasterizer="drawbot", **kwargs) self.self_rasterizing = True def normalize_result(self, pens): return pens def run(self, render_pass, renderer_state): from coldtype.pens.drawbotpen import DrawBotPen use_pool = True if use_pool: pool = AppKit.NSAutoreleasePool.alloc().init() try: db.newDrawing() if renderer_state and renderer_state.previewing: ps = renderer_state.preview_scale db.size(self.rect.w*ps, self.rect.h*ps) db.scale(ps, ps) if not renderer_state.renderer.source_reader.config.window_transparent: P().rect(self.rect).f(self.bg).cast(DrawBotPen).draw() else: db.size(self.rect.w, self.rect.h) if self.render_bg: P().rect(self.rect).f(self.bg).cast(DrawBotPen).draw() if self.rstate: render_pass.fn(*render_pass.args, renderer_state) else: render_pass.fn(*render_pass.args) result = None if renderer_state and renderer_state.previewing: previews = (render_pass.output_path.parent / "_previews") previews.mkdir(exist_ok=True, parents=True) preview_frame = previews / render_pass.output_path.name db.saveImage(str(preview_frame)) result = preview_frame else: render_pass.output_path.parent.mkdir(exist_ok=True, parents=True) db.saveImage(str(render_pass.output_path)) result = render_pass.output_path db.endDrawing() finally: if use_pool: del pool return result def notebook_display(self, scale=0.5): from base64 import b64encode from IPython.display import display, HTML for p in self.passes(Action.PreviewIndices, None): self.run(p, None) b64 = b64encode(p.output_path.read_bytes()).decode("utf-8") display(HTML(f"")) class drawbot_animation(drawbot_renderable, animation): def passes(self, action, renderer_state, indices=[]): return animation.passes(self, action, renderer_state, indices) if action in [ Action.RenderAll, Action.RenderIndices, Action.RenderWorkarea]: frames = super().active_frames(action, renderer_state, indices) passes = [] for i in frames: p = RenderPass(self, action, i, [Frame(i, self)]) passes.append(p) return passes else: return super().passes(action, renderer_state, indices) # deprecated alias drawbot_script = drawbot_renderable def dbdraw(p:P): p.cast(DrawBotPen).draw() return p def dbdraw_plain(p:P): p.cast(DrawBotPen).draw(attrs=False) return p def tobp(p:P): bp = db.BezierPath() p.replay(bp) return bp def dbdraw_with_filters(rect:Rect, filters): def _draw_call(p:P): p.cast(DrawBotPen).draw_with_filters(rect, filters) return p return _draw_call def page_rect() -> Rect: return Rect(db.width(), db.height()) @contextlib.contextmanager def new_page(r:Rect=Rect(1000, 1000)): _r = Rect(r) db.newPage(*_r.wh()) yield _r @contextlib.contextmanager def new_drawing(rect:Rect=Rect(1000, 1000), count=1, save_to=None): db.newDrawing() for idx in range(0, count): with new_page(rect) as r: yield idx, r if save_to: db.saveImage(str(save_to)) db.endDrawing() def pdfdoc(fn, path, frame_class=Frame): db.newDrawing() r = fn.rect w, h = r.wh() if hasattr(fn, "duration"): for idx in range(0, fn.duration): print(f"Saving page {idx}...") db.newPage(w, h) if frame_class: fn.func(frame_class(idx, fn)) else: fn.func(r) else: db.newPage(w, h) fn.func(r) pdf_path = Path(path) pdf_path.parent.mkdir(exist_ok=True) db.saveImage(str(pdf_path)) print("Saved pdf", str(pdf_path)) db.endDrawing() ================================================ FILE: packages/coldtype-core/src/coldtype/fx/chainable.py ================================================ class Chainable(): def __init__(self, func) -> None: self.func = func # def __or__(self, other): # print("L HELLO") # def __ror__(self, other): # print("R HELLO") ================================================ FILE: packages/coldtype-core/src/coldtype/fx/diagram.py ================================================ from coldtype.runon.path import P from coldtype.geometry.rect import Rect, txt_to_edge def arrowhead(pt, arrow, x=18, y=13): ah = P().m(pt) if arrow == "←": ah.l(pt.o(x, y)).l(pt.o(x, -y)).t(-7, 0) elif arrow == "→": ah.l(pt.o(-x, y)).l(pt.o(-x, -y)).t(7, 0) elif arrow == "↓": ah.l(pt.o(-y, x)).l(pt.o(y, x)).t(0, -7) elif arrow == "↑": ah.l(pt.o(-y, -x)).l(pt.o(y, -x)).t(0, 7) ah.cp().f(0)#.fssw(0, 1, 1.5) return ah def connection(a, b, spec="→←"): start = spec[0] end = spec[1] arrow = spec[2] if len(spec) > 2 else None corner = spec[3] if len(spec) > 3 else None src = a.ambit().point(start) dst = b.ambit().point(end) if src.x == dst.x or src.y == dst.y: line = P().m(src).l(dst) else: corn = Rect.FromPoints(src, dst).point(corner) line = P().m(src).l(corn).l(dst) line.ep().fssw(-1, 0, 2) lines = P() lines.append(line) if arrow is not None: ar = dst #line.ambit().point(arrow) lines.insert(0, arrowhead(ar, arrow)) return lines def interconnect(spec="→←→"): def _interconnect(ps): lines = P() for idx, p in enumerate(ps[:-1]): lines += connection(p, ps[idx+1], spec) return ps.append(lines) return _interconnect def ujoin(a, b, side="→", d=100, arrow=None): a_pt = a.ambit().point(side) b_pt = b.ambit().point(side) ab = Rect.FromPoints(a_pt, b_pt).expand(d, txt_to_edge(side)) l = ab.edge(txt_to_edge(side)) bar = P() if side in "←→": if a_pt.y < b_pt.y: bar.m(a_pt).l(l.ps).l(l.pn).l(b_pt).ep() else: bar.m(b_pt).l(l.ps).l(l.pn).l(a_pt).ep() out = P() if arrow: if len(arrow) > 1: a_arrow = arrow[0] b_arrow = arrow[1] else: a_arrow, b_arrow = arrow, arrow if a_arrow != "-": out.insert(0, arrowhead(a_pt, a_arrow)) if b_arrow != "-": out.insert(0, arrowhead(b_pt, b_arrow)) return out.append(bar.fssw(-1, 0, 2)) ================================================ FILE: packages/coldtype-core/src/coldtype/fx/motion.py ================================================ from noise import pnoise1 def filmjitter(doneness, base=0, speed=(10, 20), scale=(2, 3), octaves=16): """ An easy way to make something move in a way reminiscent of misregistered film """ def _filmjitter(pen): nx = pnoise1(doneness*speed[0], base=base, octaves=octaves) ny = pnoise1(doneness*speed[1], base=base+10, octaves=octaves) return pen.translate(nx * scale[0], ny * scale[1]) return _filmjitter ================================================ FILE: packages/coldtype-core/src/coldtype/fx/shapes.py ================================================ import math def sine(r, periods): """Sine-wave primitive""" def _record(pen): dp = type(pen)() pw = r.w / periods p1 = r.point("SW") end = r.point("SE") dp.moveTo(p1) done = False up = True while not done: h = r.h if up else -r.h c1 = p1.offset(pw/2, 0) c2 = p1.offset(pw/2, h) p2 = p1.offset(pw, h) dp.curveTo(c1, c2, p2) p1 = p2 if p1.x >= end.x: done = True else: done = False up = not up pen.record(dp) return _record def standingwave(r, periods, direction=1): """Standing-wave primitive""" def _record(pen): dp = type(pen)() blocks = r.subdivide(periods, "minx") for idx, block in enumerate(blocks): n, e, s, w = block.take(1, "centery").cardinals() if idx == 0: dp.moveTo(w) if direction == 1: if idx%2 == 0: dp.lineTo(n) else: dp.lineTo(s) else: if idx%2 == 0: dp.lineTo(s) else: dp.lineTo(n) if idx == len(blocks) - 1: dp.lineTo(e) dp.endPath().smooth() dp.value = dp.value[:-1] dp.endPath() pen.record(dp) return _record def polygon(sides, rect): """Polygon primitive; WIP""" def _record(pen): radius = rect.square().w / 2 c = rect.center() one_segment = math.pi * 2 / sides points = [(math.sin(one_segment * i) * radius, math.cos(one_segment * i) * radius) for i in range(sides)] dp = type(pen)() points.reverse() dp.moveTo(points[0]) for p in points[1:]: dp.lineTo(p) dp.closePath() dp.align(rect) pen.record(dp) return _record def _lissajous_points(a, b, phase, radius, num_steps=340): """I believe originally by Just van Rossum, via Very Cool Studio""" points = [] for i in range(num_steps): angle = 2 * math.pi * i / num_steps x = radius * math.sin(a * angle + phase) y = -radius * math.sin(b * angle) points.append((x, y)) return points def lissajous(a, b, phase_t, radius, num_steps=340, autophase=True): """draw a lissajous curve on the pen, though you'll probably need to align it""" def _lissajous(pen): return (pen .line(_lissajous_points(a, b, 2 * math.pi * phase_t if autophase else phase_t, radius, num_steps)) .closePath()) return _lissajous ================================================ FILE: packages/coldtype-core/src/coldtype/fx/skia.py ================================================ from fontTools.pens.recordingPen import RecordingPen import skia, tempfile, math from subprocess import run from functools import reduce from random import Random, randint from pathlib import Path from fontTools.svgLib import SVGPath from fontTools.misc.transform import Transform from coldtype.fx.chainable import Chainable from coldtype.color import normalize_color, bw from coldtype.img.blendmode import BlendMode from coldtype.runon.path import P from coldtype.pens.skiapen import SkiaPen from coldtype.geometry import Rect import coldtype.skiashim as skiashim SKIA_CONTEXT = None class Skfi(): @staticmethod def contrast_cut(mp=127, w=5): ct = bytearray(256) for i in range(256): if i < mp - w: ct[i] = 0 elif i < mp: ct[i] = int((255.0/2)*(1-(mp-i)/w)) elif i == mp: ct[i] = 127 elif i < mp + w: ct[i] = int(127+(255.0/2)*((i-mp)/w)) else: ct[i] = 255 return ct @staticmethod def as_filter(lut, a=1, r=0, g=0, b=0): args = [lut if x else None for x in [a, r, g, b]] return skia.TableColorFilter.MakeARGB(*args) @staticmethod def compose(*filters): # TODO check if ColorFilter or something else? return reduce(lambda acc, el: skia.ColorFilters.Compose(el, acc) if acc else el, reversed(filters), None) @staticmethod def fill(color): r, g, b, a = color return skia.ColorFilters.Matrix([ 0, 0, 0, 0, r, 0, 0, 0, 0, g, 0, 0, 0, 0, b, 0, 0, 0, a, 0, ]) @staticmethod def blur(blur): try: xblur, yblur = blur except: xblur, yblur = blur, blur return skiashim.imageFilters_Blur(xblur, yblur) @staticmethod def improved_noise(e, xo=0, yo=0, xs=1, ys=1, base=1): return skiashim.make_improved_noise(e, xo, yo, xs, ys, 0.015, 0.015, 3, base) # straight from the chromium source https://chromium.googlesource.com/chromium/blink/+/refs/heads/main/Source/platform/graphics/filters/FEColorMatrix.cpp @staticmethod def huerotate(hue): cosHue = math.cos(hue * math.pi / 180); sinHue = math.sin(hue * math.pi / 180); matrix = [0 for _ in range(0, 20)] matrix[0] = 0.213 + cosHue * 0.787 - sinHue * 0.213; matrix[1] = 0.715 - cosHue * 0.715 - sinHue * 0.715; matrix[2] = 0.072 - cosHue * 0.072 + sinHue * 0.928; matrix[3] = matrix[4] = 0; matrix[5] = 0.213 - cosHue * 0.213 + sinHue * 0.143; matrix[6] = 0.715 + cosHue * 0.285 + sinHue * 0.140; matrix[7] = 0.072 - cosHue * 0.072 - sinHue * 0.283; matrix[8] = matrix[9] = 0; matrix[10] = 0.213 - cosHue * 0.213 - sinHue * 0.787; matrix[11] = 0.715 - cosHue * 0.715 + sinHue * 0.715; matrix[12] = 0.072 + cosHue * 0.928 + sinHue * 0.072; matrix[13] = matrix[14] = 0; matrix[15] = matrix[16] = matrix[17] = 0; matrix[18] = 1; matrix[19] = 0; return skia.ColorFilters.Matrix(matrix) @staticmethod def saturate(s): matrix = [0 for _ in range(0, 20)] matrix[0] = 0.213 + 0.787 * s matrix[1] = 0.715 - 0.715 * s matrix[2] = 0.072 - 0.072 * s matrix[3] = matrix[4] = 0 matrix[5] = 0.213 - 0.213 * s matrix[6] = 0.715 + 0.285 * s matrix[7] = 0.072 - 0.072 * s matrix[8] = matrix[9] = 0 matrix[10] = 0.213 - 0.213 * s matrix[11] = 0.715 - 0.715 * s matrix[12] = 0.072 + 0.928 * s matrix[13] = matrix[14] = 0 matrix[15] = matrix[16] = matrix[17] = 0 matrix[18] = 1 matrix[19] = 0 return skia.ColorFilters.Matrix(matrix) @staticmethod def temp(t): return skia.ColorFilters.Matrix([ 1+t, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1-t, 0, 0, 0, 0, 0, 1, 0, ]) @staticmethod def tone(t): return skia.ColorFilters.Matrix([ 1+t, 0, 0, 0, 0, 0, 1-t, 0, 0, 0, 0, 0, 1+t, 0, 0, 0, 0, 0, 1, 0, ]) @staticmethod def expose(t): return skia.ColorFilters.Matrix([ t, 0, 0, 0, 0, 0, t, 0, 0, 0, 0, 0, t, 0, 0, 0, 0, 0, 1, 0, ]) @staticmethod def red(): return skia.ColorFilters.Matrix([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, ]) @staticmethod def green(): return skia.ColorFilters.Matrix([ 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, ]) @staticmethod def blue(): return skia.ColorFilters.Matrix([ 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, ]) @staticmethod def invert(): return skia.ColorFilters.Matrix([ -1, 0, 0, 0, 1, # Red 0, -1, 0, 0, 1, # Green 0, 0, -1, 0, 1, # Blue 0, 0, 0, 1, 0 # Alpha (unchanged) ]) # CHAINABLES def spackle(xo=None, yo=None, xs=0.85, ys=0.85, base=None, cut=240, cutw=5, fill=bw(1) ): r1 = Random() r1.seed(0) if not xo: xo = r1.randint(-500, 500) if not yo: yo = r1.randint(-500, 500) if base is None: base = randint(0, 5000) def _spackle(pen): return (pen.f(1) .attr(skp=dict( Shader=(Skfi.improved_noise(1, xo=xo, yo=yo, xs=xs, ys=ys, base=base) .makeWithColorFilter(Skfi.compose( Skfi.fill(fill), Skfi.as_filter(Skfi.contrast_cut(cut, cutw)), skia.LumaColorFilter.Make())))))) return _spackle def fill(c): """Chainable function for filling everything in pen/image-on-pen with a single color""" c = normalize_color(c) def _fill(pen): return pen.attr(skp=dict( ColorFilter=Skfi.fill(c))) return _fill def blur(x): """Chainable function for blurring everything in pen/image-on-pen""" def _blur(pen): return pen.attr(skp=dict( ImageFilter=Skfi.blur(x))) return _blur try: import potrace as potracer turn_policy = potracer.POTRACE_TURNPOLICY_MINORITY except ImportError: turn_policy = None pass def potrace(rect, invert=True, turdsize=2, # 0-infinity turnpolicy=turn_policy, alphamax=1.0, # 0.0 (polygon) to 1.3333 (no corners) opticurve=1, opttolerance=0.2, # 0 -1 ): """Chainable function for tracing a pen/image-on-pen ; can be combined with a previous call to phototype for better control of blurring/edges""" from PIL import Image def _potrace(pen): if invert: pen = pen.copy().layer(1, lambda _: P(rect).f(1).blendmode(BlendMode.Difference)) res = SkiaPen.Precompose(pen, rect, SKIA_CONTEXT) pilimg = Image.fromarray(res.convert(alphaType=skia.kUnpremul_AlphaType)) bmp = potracer.Bitmap(pilimg) path = bmp.trace(turdsize, turnpolicy, alphamax, opticurve, opttolerance) op = P() for curve in path: op.moveTo(curve.start_point.x, curve.start_point.y) for segment in curve: if segment.is_corner: op.lineTo(segment.c.x, segment.c.y) else: op.curveTo( (segment.c1.x, segment.c1.y), (segment.c2.x, segment.c2.y), (segment.end_point.x, segment.end_point.y)) op.closePath() op.scale(1, -1, point=rect.pc) return op return _potrace def precompose(rect, placement=None, opacity=1, scale=1, disk=False, style=None, ): def _precompose(pen): img = SkiaPen.Precompose(pen, rect, context=SKIA_CONTEXT, scale=scale, disk=disk, style=style) return (P() .rect(placement or rect) .img(img, (placement or rect), False, opacity) .f(None)) return Chainable(_precompose) def rasterized(rect, scale=1, wrapped=False): """ Same as precompose but returns the Image created rather than setting that image as the attr-image of this pen """ from coldtype.img.skiaimage import SkiaImage def _rasterized(pen): precomposed = SkiaPen.Precompose(pen, rect, scale=scale, context=SKIA_CONTEXT, disk=False) if wrapped: return SkiaImage(precomposed) else: return precomposed return _rasterized, dict(returns=SkiaImage if wrapped else skia.Image) def rasterize(rect, path): def _rasterize(pen): pen.ch(precompose(rect)).img().get("src").save(str(Path(path).expanduser()), skia.kPNG) return None return _rasterize def mod_pixels(rect, scale=0.1, mod=lambda rgba: None): import PIL.Image def _mod_pixels(pen): raster = pen.ch(rasterized(rect, scale=scale)) pi = PIL.Image.fromarray(raster, "RGBa") for x in range(pi.width): for y in range(pi.height): r, g, b, a = pi.getpixel((x, y)) res = mod((r, g, b, a)) if res: pi.putpixel((x, y), tuple(res)) out = skia.Image.frombytes(pi.convert('RGBA').tobytes(), pi.size, skia.kRGBA_8888_ColorType) return (P() .rect(rect) .img(out, rect, False) .f(None)) return _mod_pixels def vector_pixels(rect, scale=0.1, lut=dict(), combine=True, print_misses=True, shaper=lambda r: P().rect(r)): import PIL.Image from collections import defaultdict def _vector_pixels(pen): raster = pen.ch(rasterized(rect, scale=scale)) pi = PIL.Image.fromarray(raster, "RGBa") layers = defaultdict(lambda: P()) for x in range(pi.width): for y in range(pi.height): r, g, b, a = pi.getpixel((x, y)) res = (r, g, b, a) if any(res): lookup = (int(r/255*100), int(g/255*100), int(b/255*100), int(a/255*100)) if lookup in lut: color = lut[lookup] else: if print_misses: print(lookup) color = (r/255, g/255, b/255, a/255) (layers[color] .append(shaper(Rect(x, pi.height-y, 1, 1)) .f(color))) out = P(layers.values()).scale(1/scale, point=(0, 0)) if combine: return out.map(lambda p: p.pen()) else: return out return _vector_pixels def warp_image(image, rect, mod): from skimage import img_as_ubyte from skimage.transform import warp import numpy as np image_data = image.toarray() image_data = image_data.reshape(rect.h, rect.w, 4) image_rgb = image_data[..., :3] image_rgb_uint8 = img_as_ubyte(image_rgb) swirled = warp(image_rgb_uint8, mod, output_shape=image_rgb_uint8.shape) swirled_uint8 = (swirled * 255).astype(np.uint8) image_rgb_uint8 = swirled_uint8 height, width, channels = image_rgb_uint8.shape if channels == 3: image_rgba = np.dstack((image_rgb_uint8, np.full((height, width), 255, dtype=np.uint8))) else: image_rgba = image_rgb_uint8 return skia.Image.fromarray(image_rgba) def warp(rect, mod): from skimage import img_as_ubyte from skimage.transform import warp import numpy as np def _warp(pen): raster = pen.ch(rasterized(rect)) image_data = raster.toarray() image_data = image_data.reshape(rect.w, rect.h, 4) image_rgb = image_data[..., :3] image_rgb_uint8 = img_as_ubyte(image_rgb) swirled = warp(image_rgb_uint8, mod, output_shape=image_rgb_uint8.shape) swirled_uint8 = (swirled * 255).astype(np.uint8) image_rgb_uint8 = swirled_uint8 height, width, channels = image_rgb_uint8.shape if channels == 3: image_rgba = np.dstack((image_rgb_uint8, np.full((height, width), 255, dtype=np.uint8))) else: image_rgba = image_rgb_uint8 out = skia.Image.fromarray(image_rgba) return (P() .rect(rect) .img(out, rect, False) .f(None)) return _warp def luma(rect, fill=None): """Chainable function for converting light part of pen/image-on-pen into an alpha channel; see `LumaColorFilter `_""" def _luma(pen): pen = pen.ch(precompose(rect)) pen.attr(skp=dict(ColorFilter=skia.LumaColorFilter.Make())) if fill is not None: pen = (pen .ch(precompose(rect)) .attr(skp=dict(ColorFilter=Skfi.fill(normalize_color(fill))))) return pen return _luma def phototype(rect=None, blur=5, cut=127, cutw=3, fill=1, rgba=[0, 0, 0, 1], luma=True ): """Chainable function for “exposing” a pen/image-on-pen in the manner of lithofilm, i.e. light-parts are kept, dark parts are thrown away (turned into alpha)""" def _phototype(pen): nonlocal rect if rect is None: rect = pen.ambit().inset(-50) r, g, b, a = rgba first_pass = dict(ImageFilter=Skfi.blur(blur)) if luma: first_pass["ColorFilter"] = skia.LumaColorFilter.Make() cut_filters = [ Skfi.as_filter( Skfi.contrast_cut(cut, cutw), a=a, r=r, g=g, b=b)] if fill is not None: cut_filters.append(Skfi.fill(normalize_color(fill))) return (pen .ch(precompose(rect)) .attr(skp=first_pass) .ch(precompose(rect)) .attr(skp=dict( ColorFilter=Skfi.compose(*cut_filters)))) return Chainable(_phototype) def color_phototype(rect, blur=5, cut=127, cutw=15, rgba=[1, 1, 1, 1] ): return phototype(rect, blur, 255-cut, cutw, fill=None, rgba=rgba, luma=False) def huerotate(c): c = c*360%360 def _fill(pen): return pen.attr(skp=dict(ColorFilter=Skfi.huerotate(c))) return _fill def saturate(c): #c = c*360%360 def _fill(pen): return pen.attr(skp=dict(ColorFilter=Skfi.saturate(c))) return _fill def expose(t): def _fill(pen): return pen.attr(skp=dict(ColorFilter=Skfi.expose(t))) return _fill def invert(): def _fill(pen): return pen.attr(skp=dict(ColorFilter=Skfi.invert())) return _fill def temp(t): def _fill(pen): return pen.attr(skp=dict(ColorFilter=Skfi.temp(t))) return _fill def tone(t): def _fill(pen): return pen.attr(skp=dict(ColorFilter=Skfi.tone(t))) return _fill def temptone(temp, tone): def _fill(pen): pen.attr(skp=dict(ColorFilter=Skfi.compose(Skfi.temp(temp), Skfi.tone(tone)))) return _fill def channel(c): filters = dict(red=Skfi.red, green=Skfi.green, blue=Skfi.blue) if isinstance(c, str): filter = filters[c] else: filter = list(filters.values())[c] def _fill(pen): pen.attr(skp=dict(ColorFilter=filter())) return _fill def rgbmod(rect, r=None, g=None, b=None): def _rgbmod(p): raster = p.ch(rasterized(rect, wrapped=True)) return (P( raster.copy() .in_pen() .f(-1) .ch(channel(0)) .ch(luma(rect, fill=1)) .ch(r) if r else None, raster.copy() .in_pen() .f(-1) .ch(channel(1)) .ch(luma(rect, fill=1)) .ch(g) if g else None, raster.copy() .in_pen() .f(-1) .ch(channel(2)) .ch(luma(rect, fill=1)) .ch(b) if b else None, )) return _rgbmod def shake(seg_length=2, deviation=2, seed=0): """shake up the path""" def _shake(p): effect = skia.DiscretePathEffect.Make( seg_length, deviation, seed) return p.attr(skp=dict(PathEffect=effect)) return _shake def round_corners(roundedness=20): def _round(p): effect = skia.CornerPathEffect.Make(roundedness) return p.attr(skp=dict(PathEffect=effect)) return _round def draw_canvas(r:Rect, draw_function:callable, in_pen=False): from coldtype.img.skiaimage import SkiaImage from coldtype.fx.skia import SKIA_CONTEXT from coldtype.pens.skiapen import SkiaPen def draw(canvas): return draw_function(r, canvas) img = SkiaImage(SkiaPen.Precompose(draw, r, context=SKIA_CONTEXT)) if in_pen: return img.in_pen().f(-1) else: return img def text_image(r:Rect, in_pen=True): """ Requires that annotate=1 is passed to original StSt or Style """ from coldtype.img.skiaimage import SkiaImage def _text_image(p:P): try: stst = p._stst text = stst.text except AttributeError: try: stst = p.parent()._stst text = p.data("glyphName") # TODO this isn't a good assumption except AttributeError: stst = None text = None if not stst: print("! please pass annotate=1 to original `StSt` !") return p style = stst.style skia_font = skia.Font(skia.Typeface.MakeFromFile(str(style.font.path)), style.fontSize) builder = skia.TextBlobBuilder() builder.allocRun(text, skia_font, 0, 0) blob = builder.make() x, y = p.ambit(tx=0, ty=0).xy() def draw(r, canvas): canvas.drawTextBlob(blob, x, r.h-y, skia.Paint(AntiAlias=True)) return draw_canvas(r, draw, in_pen=in_pen) return _text_image, dict(returns=P if in_pen else SkiaImage) def image_shader(r:Rect, image, sksl, in_pen=True): """ Requires that annotate=1 is passed to original StSt or Style """ from coldtype.img.skiaimage import SkiaImage def _image_shader(p:P): def draw(r, canvas): imageShader = image.makeShader(skia.SamplingOptions(skia.FilterMode.kLinear)) effect = skia.RuntimeEffect.MakeForShader(sksl) children = skia.RuntimeEffectChildPtr(imageShader) runtime_effect = skia.SpanRuntimeEffectChildPtr(children, 1) myShader = effect.makeShader(None, runtime_effect) paint = skia.Paint() paint.setShader(myShader) canvas.drawPaint(paint) return draw_canvas(r, draw, in_pen=in_pen) return _image_shader, dict(returns=P if in_pen else SkiaImage) FREEZES = {} from inspect import getsource def freeze(do_freeze, as_image, callback, additionals=[]): """ Use this function to “freeze” the result of another (expensive) function as an image, so you don’t have to recompute the same thing over and over N.B. Because of a quirk with Python’s introspection facilities, a multi-line lambda will work with freeze, but the cache will not update if code changes on any line but the first; if you need a multi-line function, pass in a def and all the code will be read as a cache key """ def getsrc(f): _src = getsource(f) if "lambda:" in _src: _src = "lambda:".join(_src.split("lambda:")[1:]).strip() return _src src = getsrc(callback) for a in additionals: if callable(a): src += getsrc(a) else: src += repr(a) if not src: print("NO SRC FOUND FOR FREEZE") if not do_freeze: if src in FREEZES: del FREEZES[src] return callback() if src in FREEZES: currently_image, res = FREEZES[src] if as_image != currently_image: del FREEZES[src] if src not in FREEZES: #print("FREEZING", src) res = callback() if as_image: res_img = res.ch(precompose(res.bounds().inset(-100))) FREEZES[src] = [1, res_img] else: FREEZES[src] = [0, res] return FREEZES[src][-1] ================================================ FILE: packages/coldtype-core/src/coldtype/fx/warping.py ================================================ import math from coldtype.beziers import CurveCutter, splitCubicAtT try: import noise except ImportError: raise Exception("`pip install noise`") def warp_fn(xa=0, ya=-1, xs=300, ys=300, speed=5, base=0, octaves=1, mult=50, rz=1024): if ya == -1: ya = xa def warp(x, y): _x = (x+xa)/xs _y = (y+ya)/ys pn = noise.pnoise3(_x, _y, speed, octaves=octaves, base=base, repeatz=rz) return x+pn*mult, y+pn*mult return warp def warp(flatten=10, xa=0, ya=-1, xs=300, ys=300, speed=5, base=0, octaves=1, mult=50, rz=1024): """Chainable function for warping a pen""" def _warp(pen): if flatten is not None and flatten > 0: pen.flatten(flatten) pen.nlt(warp_fn(xa, ya, xs, ys, speed, base, octaves, mult, rz)) return _warp def bend(pen, curve, tangent=True): def _bend(pen): cc = CurveCutter(curve) ccl = cc.length dpl = pen.bounds().point("SE").x xf = ccl/dpl def bender(x, y): p, tan = cc.subsegmentPoint(end=x*xf) px, py = p if tangent: a = math.sin(math.radians(180+tan)) * y b = math.cos(math.radians(180+tan)) * y return (px+a, py+b) #return (px, y+py) else: return (px, y+py) return pen.nonlinear_transform(bender) return _bend def bend2(curve, tangent=True, offset=(0, 1)): def _bend(pen): bw = pen.bounds().w a = curve.value[0][-1][0] b, c, d = curve.value[1][-1] def bender(x, y): c1, c2 = splitCubicAtT(a, b, c, d, offset[0] + (x/bw)*offset[1]) _, _a, _b, _c = c1 if tangent: tan = math.degrees(math.atan2(_c[1] - _b[1], _c[0] - _b[0]) + math.pi*.5) ax = math.sin(math.radians(90-tan)) * y by = math.cos(math.radians(90-tan)) * y return _c[0]+ax, (y+_c[1])+by return _c[0], y+_c[1] return pen.nonlinear_transform(bender) return _bend def bend3(curve, tangent=False, offset=(0, 1)): def _bend(pen): a = curve.value[0][-1][0] b, c, d = curve.value[1][-1] bh = pen.bounds().h def bender(x, y): c1, c2 = splitCubicAtT(a, b, c, d, offset[0] + (y/bh)*offset[1]) _, _a, _b, _c = c1 if tangent: tan = math.degrees(math.atan2(_c[1] - _b[1], _c[0] - _b[0]) + math.pi*.5) ax = math.sin(math.radians(90-tan)) * y by = math.cos(math.radians(90-tan)) * y return x+_c[0]+ax, (y+_c[1])+by return x+_c[0], _c[1] return pen.nonlinear_transform(bender) return _bend ================================================ FILE: packages/coldtype-core/src/coldtype/fx/xray.py ================================================ from coldtype.runon.path import P as P from coldtype.geometry import Point from coldtype.color import hsl def skeletonLookup(self) -> dict: m, l, c, cf, q, qf, cbs, qbs = [[] for x in range(8)] for idx, (mv, pts) in enumerate(self._val.value): pts = [Point(x) for x in pts] if mv == "moveTo": m.append(pts[0]) elif mv == "lineTo": l.append(pts[0]) elif mv in ["curveTo", "qCurveTo"]: lp = Point(self._val.value[idx-1][-1][-1]) pts.insert(0, lp) onc = pts[-1] if mv == "curveTo": c.append(onc) cf.extend([pts[1], pts[2]]) cbs.append([pts[0], pts[1]]) cbs.append([pts[-2], onc]) else: q.append(onc) for j, _p in enumerate(pts[:-2]): np = pts[j+1] qbs.append([_p, np]) qf.append(np) qbs.append([pts[-2], onc]) return { "moveTo": m, "lineTo": l, "curveOn": c, "curveOff": cf, "qCurveOn": q, "qCurveOff": qf, "curveBars": cbs, "qCurveBars": qbs } scaleables = [ "moveTo", "lineTo", "curveOn", "curveOff", "qCurveOn", "qCurveOff", ] def skeleton(scale=1): def _skeleton(p:P): pts = list(skeletonLookup(p).values()) return (P( (P().enumerate(pts[0], lambda x: P().r(x.el.r(30))) .tag("moveTo")), (P().enumerate(pts[1], lambda x: P().rect(x.el.r(20))) .tag("lineTo")), (P().enumerate(pts[2], lambda x: P().oval(x.el.r(20))) .tag("curveOn")), (P().enumerate(pts[3], lambda x: P().oval(x.el.r(10))) .tag("curveOff")), (P().enumerate(pts[4], lambda x: P().oval(x.el.r(20))) .tag("qCurveOn")), (P().enumerate(pts[5], lambda x: P().oval(x.el.r(10))) .tag("qCurveOff")), (P().enumerate(pts[6], lambda x: P().line(x.el)) .tag("curveBars")), (P().enumerate(pts[7], lambda x: P().line(x.el)) .tag("qCurveBars"))) .find( lambda e: e.tag() in scaleables, lambda e: e.mapv(lambda x: x.scale(scale))) .fssw(-1, 0, 2)) return _skeleton ================================================ FILE: packages/coldtype-core/src/coldtype/geometry/__init__.py ================================================ from coldtype.geometry.primitives import calc_angle from coldtype.geometry.geometrical import Geometrical from coldtype.geometry.atom import Atom from coldtype.geometry.point import Point from coldtype.geometry.line import Line from coldtype.geometry.curve import Curve from coldtype.geometry.edge import Edge, txt_to_edge from coldtype.geometry.rect import Rect, align Pt = Point Rt = Rect def Geo(*args): if len(args) == 1: return Atom(args[0]) elif len(args) == 2: if isinstance(args[0], Point): return Line(*args) else: return Point(*args) elif len(args) == 4: return Rect(*args) ================================================ FILE: packages/coldtype-core/src/coldtype/geometry/atom.py ================================================ from coldtype.geometry.geometrical import Geometrical class Atom(Geometrical): def __init__(self, x): self.x = x __hash__ = object.__hash__ def __eq__(self, o): # TODO isclose? try: return self.x == o.x except AttributeError: return self.x == o def __repr__(self): return f"Atom({self.x})" def __getitem__(self, key): if key == 0: return self.x else: raise TypeError("Index must be 0") def __len__(self): return 1 def __setitem__(self, key, value): if key == 0: self.x = value else: raise IndexError( "Invalid index for atom assignment, must be 0") def reverse(self): return self ================================================ FILE: packages/coldtype-core/src/coldtype/geometry/curve.py ================================================ from coldtype.geometry.line import Line from coldtype.geometry.point import Point from fontTools.misc.bezierTools import calcCubicArcLength, splitCubicAtT class Curve(Line): def __init__(self, start, cp1, cp2, end): self.cp1 = cp1 self.cp2 = cp2 super().__init__(start, end) @property def abcd(self): return [self.start, self.cp1, self.cp2, self.end] def pts(self): return self.abcd def split_t(self, t): l1, l2 = splitCubicAtT(*self.abcd, t) return Curve(*l1), Curve(*l2) def t(self, t): l1, l2 = self.split_t(t) return Point(*l2.start) def length(self): return calcCubicArcLength(*self.abcd) def split_tpx(self, tpx): return self.split_t(tpx/self.length()) def tpx(self, tpx, limit=True): return self.t(tpx/self.length()) def __eq__(self, l): if not hasattr(l, "start") or not hasattr(l, "cp1"): return False return self.start == l.start and self.end == l.end and self.cp1 == l.cp1 and self.cp2 == l.cp2 def reverse(self): return Curve(*list(reversed(self.abcd))) def tan_out(self): return Line(self.cp2, self.end) def tan_in(self): return Line(self.start, self.cp1) def inset(self, px): inset_start = self.split_tpx(px)[1] return inset_start.reverse().split_tpx(px)[1].reverse() def __repr__(self): pts = ','.join([str(p) for p in self.abcd]) return f"Curve({pts})" def __len__(self): return 4 def __getitem__(self, idx): if idx == 0: return self.start elif idx == 1: return self.cp1 elif idx == 2: return self.cp2 elif idx == 3: return self.end else: raise IndexError("Curve only has four points") ================================================ FILE: packages/coldtype-core/src/coldtype/geometry/edge.py ================================================ from enum import Enum class Edge(Enum): MaxY = 1 MaxX = 2 MinY = 3 MinX = 4 CenterY = 5 CenterX = 6 def PairFromCompass(cmp): if isinstance(cmp, Edge): return None if isinstance(cmp, str): cmp = cmp.upper() if cmp in ["C", "•"]: return (Edge.CenterX, Edge.CenterY) elif cmp in ["W", "←"]: return (Edge.MinX, Edge.CenterY) elif cmp in ["NW", "↖"]: return (Edge.MinX, Edge.MaxY) elif cmp in ["N", "↑"]: return (Edge.CenterX, Edge.MaxY) elif cmp in ["NE", "↗"]: return (Edge.MaxX, Edge.MaxY) elif cmp in ["E", "→"]: return (Edge.MaxX, Edge.CenterY) elif cmp in ["SE", "↘"]: return (Edge.MaxX, Edge.MinY) elif cmp in ["S", "↓"]: return (Edge.CenterX, Edge.MinY) elif cmp in ["SW", "↙"]: return (Edge.MinX, Edge.MinY) def txt_to_edge(txt): if isinstance(txt, str): txt = txt.lower() if txt in ["maxy", "mxy", "n", "⊤", "↑"]: return Edge.MaxY elif txt in ["maxx", "mxx", "e", "⊣", "→"]: return Edge.MaxX elif txt in ["miny", "mny", "s", "⊥", "↓"]: return Edge.MinY elif txt in ["minx", "mnx", "w", "⊢", "←"]: return Edge.MinX elif txt in ["centery", "cy", "midy", "mdy", "H"]: return Edge.CenterY elif txt in ["centerx", "cx", "midx", "mdx", "⌶"]: return Edge.CenterX else: return Edge.PairFromCompass(txt) else: return txt def edge_opposite(e): if not isinstance(e, Edge): return [edge_opposite(_e) for _e in e] if e == Edge.MaxY: return Edge.MinY elif e == Edge.MinY: return Edge.MaxY elif e == Edge.MaxX: return Edge.MinX elif e == Edge.MinX: return Edge.MaxX ================================================ FILE: packages/coldtype-core/src/coldtype/geometry/geometrical.py ================================================ class Geometrical(): pass ================================================ FILE: packages/coldtype-core/src/coldtype/geometry/line.py ================================================ import math from fontTools.misc.transform import Transform from coldtype.geometry.geometrical import Geometrical from coldtype.geometry.point import Point from coldtype.geometry.primitives import line_intersection, calc_angle, polar_coord from coldtype.interpolation import norm class Line(Geometrical): def __init__(self, start, end): self.start = Point(start) self.end = Point(end) def __eq__(self, l): if not hasattr(l, "start"): return False return self.start == l.start and self.end == l.end __hash__ = object.__hash__ def point(self, p): if p == "N": return self.pn elif p == "E": return self.pe elif p == "S": return self.ps elif p == "W": return self.pw elif p == "C": return self.mid @property def mid(self): return self.start.i(0.5, self.end) @property def mxx(self): return max([p.x for p in self.pts()]) @property def mnx(self): return min([p.x for p in self.pts()]) @property def mxy(self): return max([p.y for p in self.pts()]) @property def mny(self): return min([p.y for p in self.pts()]) @property def pe(self): return max(self.pts(), key=lambda p: p.x) @property def pw(self): return min(self.pts(), key=lambda p: p.x) @property def pn(self): return max(self.pts(), key=lambda p: p.y) @property def ps(self): return min(self.pts(), key=lambda p: p.y) def __repr__(self): return f"Line({self.start}, {self.end})" def __len__(self): return 2 def __getitem__(self, idx): if idx == 0: return self.start elif idx == 1: return self.end else: raise IndexError("Line only has two points") def reverse(self): p1, p2 = self return Line(p2, p1) def __invert__(self): return self.reverse() def t(self, t): return self.start.interp(t, self.end) def length(self): a2 = math.pow(self.start.x - self.end.x, 2) b2 = math.pow(self.start.y - self.end.y, 2) return math.sqrt(a2 + b2) @property def l(self): return self.length() def tpx(self, tpx, limit=True): x = tpx * math.cos(self.angle()) y = tpx * math.sin(self.angle()) tp = self.start.offset(x, y) if not limit: return tp else: if Line(self.start, tp).length() > self.length(): return self.end else: return tp def angle(self): return calc_angle(self.start, self.end) @property def ang(self): return self.angle()%math.pi def pts(self): return [self.start, self.end] def splat(self): return [(self.start.x, self.start.y), (self.end.x, self.end.y)] def transform(self, t): pts = self.pts() x1, x2 = [t.transformPoint(pt) for pt in pts] return Line(x1, x2) def rotate(self, degrees, point=None): if Transform: t = Transform() if not point: point = self.mid t = t.translate(point.x, point.y) t = t.rotate(math.radians(degrees)) t = t.translate(-point.x, -point.y) return self.transform(t) else: raise Exception("fontTools not installed") def bow(self, amt, t=0.5, angle=90): rotated = self.rotate(angle, point=self.t(t)) return rotated.tpx(self.length()*0.5+amt) def project(self, pt, dist, angle=90): dx, dy = polar_coord((0, 0), self.ang+math.radians(angle), dist) return self.t(pt).offset(dx, dy) def inset(self, px): return Line(self.tpx(px), self.reverse().tpx(px)) inset_y = inset inset_x = inset def extr(self, amt): p1, p2 = self return Line(p2.i(1+amt, p1), p1.i(1+amt, p2)) def offset(self, x, y): p1, p2 = self return Line(p1.offset(x, y), p2.offset(x, y)) def offset_x(self, dx): return self.offset(dx, 0) def offset_y(self, dy): return self.offset(0, dy) def tan_out(self): return self def tan_in(self): return self o = offset def __floordiv__(self, other): return self.offset(0, other) def __truediv__(self, other): return self.offset(other, 0) def __mod__(self, other): return self.offset(*other) def intersection(self, other): return Point(line_intersection(self, other)) sect = intersection def __and__(self, other): return self.intersection(other) def join(self, other): from coldtype.geometry.rect import Rect return Rect.FromPoints(self.start, self.end, other.end, other.start) def interp(self, x, other): return Line(self.start.i(x, other.start), self.end.i(x, other.end)) i = interp def setx(self, x): return Line(self.start.setx(x), self.end.setx(x)) def __mul__(self, other): return self.setx(other) def sety(self, y): return Line(self.start.sety(y), self.end.sety(y)) def __matmul__(self, other): return self.sety(other) def intersects(self, other_line:"Line") -> bool: def orientation(p, q, r): val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y) if val == 0: return 0 return 1 if val > 0 else 2 def on_segment(p, q, r): return (q.x <= max(p.x, r.x) and q.x >= min(p.x, r.x) and q.y <= max(p.y, r.y) and q.y >= min(p.y, r.y)) p1, q1 = self.start, self.end p2, q2 = other_line.start, other_line.end o1 = orientation(p1, q1, p2) o2 = orientation(p1, q1, q2) o3 = orientation(p2, q2, p1) o4 = orientation(p2, q2, q1) if o1 != o2 and o3 != o4: return True if o1 == 0 and on_segment(p1, p2, q1): return True if o2 == 0 and on_segment(p1, q2, q1): return True if o3 == 0 and on_segment(p2, p1, q2): return True if o4 == 0 and on_segment(p2, q1, q2): return True return False ================================================ FILE: packages/coldtype-core/src/coldtype/geometry/point.py ================================================ import math from coldtype.geometry.geometrical import Geometrical from coldtype.interpolation import norm from coldtype.geometry.primitives import polar_coord, line_intersection, calc_angle, calc_vector try: from fontTools.misc.transform import Transform except ImportError: Transform = None def rt(v, mult): rndd = float(round(v / mult) * mult) if rndd.is_integer(): return int(rndd) else: return rndd class Point(Geometrical): """Representation of a point (x,y), indexable""" def __init__(self, *points, rect=None, corner=None): self._rect = rect self._corner = corner if len(points) == 2: self.x = points[0] self.y = points[1] else: point = points[0] try: x, y = point self.x = x self.y = y except TypeError: try: self.x = point.x self.y = point.y except AttributeError: self.x = 0 self.y = 0 __hash__ = object.__hash__ def Z(): return Point([0, 0]) def from_obj(obj): p = Point((0, 0)) try: p.x = obj.x p.y = obj.y except: pass return p def offset(self, dx, dy): "Offset by dx, dy" return Point((self.x + dx, self.y + dy)) def offset_x(self, dx): return self.offset(dx, 0) def offset_y(self, dy): return self.offset(0, dy) o = offset def rect(self, w, h=None): "Create a rect from this point as center, with w and h dimensions provided" from coldtype.geometry.rect import Rect if h is None: h = w return Rect((self.x-w/2, self.y-h/2, w, h)) r = rect def xy(self): "As a tuple" return self.x, self.y def round(self): """round the values in the point to the nearest integer""" return Point([int(round(n)) for n in self]) def round_to(self, to=10): """round the values in the point to the nearest integer multiple""" return Point([rt(n, to) for n in self.xy()]) def inside(self, rect): mnx, mny, mxx, mxy = rect.mnmnmxmx() if mnx <= self.x <= mxx and mny <= self.y <= mxy: return True else: return False def clip(self, rect): x, y = self mnx, mny, mxx, mxy = rect.mnmnmxmx() if x < mnx: x = mnx elif x > mxx: x = mxx if y < mny: y = mny elif y > mxy: y = mxy return Point(x, y) def flip(self, frame): return Point((self.x, frame.h - self.y)) def flipSelf(self, frame): x, y = self.flip(frame) self.x = x self.y = y def scale(self, x, y=None): if not y: y = x return Point((self.x * x, self.y * y)) def transform(self, t) -> "Point": return Point(*t.transformPoint((self.x, self.y))) def rotate(self, degrees, point) -> "Point": if Transform: t = Transform() t = t.translate(point.x, point.y) t = t.rotate(math.radians(degrees)) t = t.translate(-point.x, -point.y) return self.transform(t) else: raise Exception("fontTools not installed") def join(self, other): from coldtype.geometry.line import Line return Line(self, other) def interp(self, v, other): """Interpolate with another point""" sx, sy = self ox, oy = other if not isinstance(v, float) and v != 1: v = v/100 return Point((norm(v, sx, ox), norm(v, sy, oy))) def i(self, *args): other = args if isinstance(other[0], Point): return self.interp(0.5, other[0]) else: x, pt = other return self.interp(x, pt) def project(self, angle, dist): dx, dy = polar_coord((0, 0), math.radians(angle), dist) return self.offset(dx, dy) def project_to(self, angle, line): ray = [self, self.project(angle, 1000)] return Point(line_intersection(ray, line)) def cdist(self, other): vec = calc_vector(self, other) ang = calc_angle(self, other) dist = math.sqrt(math.pow(vec[0], 2) + math.pow(vec[1], 2)) return dist, math.degrees(ang) def noop(self, *args, **kwargs): return self def __eq__(self, o): try: return all([self.x == o.x, self.y == o.y]) except: return False def __repr__(self): return "Point" + str(self.xy()) def __getitem__(self, key): return self.xy()[key] def __len__(self): return 2 def __setitem__(self, key, value): if key == 0: self.x = value elif key == 1: self.y = value else: raise IndexError( "Invalid index for point assignment, must be 0 or 1") def setx(self, x): return Point([x, self.y]) def sety(self, y): return Point([self.x, y]) def reverse(self): return Point(self.y, self.x) def __add__(self, o): return Point(self.x + o.x, self.y + o.y) def __sub__(self, o): return Point(self.x - o.x, self.y - o.y) def __mul__(self, o): return Point(self.x * o, self.y * o) ================================================ FILE: packages/coldtype-core/src/coldtype/geometry/primitives.py ================================================ import math from coldtype.geometry.edge import Edge from fontTools.misc.arrayTools import sectRect, unionRect MINYISMAXY = False #https://stackoverflow.com/questions/20677795/how-do-i-compute-the-intersection-point-of-two-lines def line_intersection(line1, line2): xdiff = (line1[0][0] - line1[1][0], line2[0][0] - line2[1][0]) ydiff = (line1[0][1] - line1[1][1], line2[0][1] - line2[1][1]) def det(a, b): return a[0] * b[1] - a[1] * b[0] div = det(xdiff, ydiff) if div == 0: raise Exception('lines do not intersect') d = (det(*line1), det(*line2)) x = det(d, xdiff) / div y = det(d, ydiff) / div return x, y def calc_vector(point1, point2): x1, y1 = point1 x2, y2 = point2 dx = x2 - x1 dy = y2 - y1 return dx, dy def calc_angle(point1, point2): dx, dy = calc_vector(point1, point2) return math.atan2(dy, dx) def polar_coord(xy, angle, distance): x, y = xy nx = x + (distance * math.cos(angle)) ny = y + (distance * math.sin(angle)) return nx, ny def centered_square_outside(rect): x, y, w, h = rect if w < h: return [x + (w - h) / 2, y, h, h] else: return [x, y + (h - w) / 2, w, w] def centered_square_inside(rect): x, y, w, h = rect if w > h: return [x + (w - h) / 2, y, h, h] else: return [x, y + (h - w) / 2, w, w] def perc_to_pix(rect, amount, edge): """ perc(entage) to pix(els) — where the percentage is a decimal between 0 and 1 — cannot be 0 or 1 """ x, y, w, h = rect if amount <= 1.0: d = h if edge == Edge.MinY or edge == Edge.MaxY or edge == Edge.CenterY else w if amount < 0: return d + amount else: return d * amount # math.floor else: return amount def divide(rect, amount, edge, forcePixel=False): x, y, w, h = rect if not forcePixel: amount = perc_to_pix(rect, amount, edge) if edge == Edge.MaxY: if MINYISMAXY: return [x, y, w, amount], [x, y + amount, w, h - amount] else: return [x, y + h - amount, w, amount], [x, y, w, h - amount] elif edge == Edge.MinY: if MINYISMAXY: return [x, y + h - amount, w, amount], [x, y, w, h - amount] else: return [x, y, w, amount], [x, y + amount, w, h - amount] elif edge == Edge.MinX: return [x, y, amount, h], [x + amount, y, w - amount, h] elif edge == Edge.MaxX: return [x + w - amount, y, amount, h], [x, y, w - amount, h] elif edge == Edge.CenterX: lw = (w - amount) / 2 return [x, y, lw, h], [x + lw, y, amount, h], [x + lw + amount, y, lw, h] elif edge == Edge.CenterY: lh = (h - amount) / 2 return [x, y, w, lh], [x, y + lh, w, amount], [x, y + lh + amount, w, lh] def subdivide(rect, count, edge, forcePixel=False): r = rect subs = [] if hasattr(count, "__iter__"): amounts = count i = len(amounts) + 1 a = 0 while i > 1: s, r = divide(r, amounts[a], edge, forcePixel=forcePixel) subs.append(s) i -= 1 a += 1 subs.append(r) return subs else: i = count while i > 1: s, r = divide(r, 1/i, edge, forcePixel=forcePixel) subs.append(s) i -= 1 subs.append(r) return subs def pieces(rect, amount, edge): x, y, w, h = rect d = w if edge == Edge.MaxX or edge == Edge.MaxY: d = h fit = math.floor(d / amount) return subdivide(rect, fit, edge) def take(rect, amount, edge, forcePixel=False): if edge == Edge.CenterX or edge == Edge.CenterY: _, r, _ = divide(rect, amount, edge, forcePixel=forcePixel) return r else: r, _ = divide(rect, amount, edge, forcePixel=forcePixel) return r def subtract(rect, amount, edge, forcePixel=False): _, r = divide(rect, amount, edge, forcePixel=forcePixel) return r def drop(rect, amount, edge): return subtract(rect, amount, edge) def inset(rect, dx, dy): x, y, w, h = rect return [x + dx, y + dy, w - (dx * 2), h - (dy * 2)] def offset(rect, dx, dy): x, y, w, h = rect if MINYISMAXY: return [x + dx, y - dy, w, h] else: return [x + dx, y + dy, w, h] def expand(rect, amount, edge): x, y, w, h = rect if edge == Edge.MinX: w += amount x -= amount elif edge == Edge.MaxX: w += amount elif edge == Edge.MinY: y -= amount h += amount elif edge == Edge.MaxY: h += amount return [x, y, w, h] def centerpoint(rect): x, y, w, h = rect return [x + w/2, y + h/2] def add(rect_a, rect_b): # TODO better/correct implementation! ax, ay, aw, ah = rect_a bx, by, bw, bh = rect_b return [ min(ax, bx), min(ay, by), aw + bw, ah ] def scale(rect, s, x_edge=Edge.CenterX, y_edge=Edge.CenterY): """ Only a partial implementation atm """ x, y, w, h = rect return [x * s, y * s, w * s, h * s] def edgepoints(rect, edge): x, y, w, h = rect if edge == Edge.MaxY: if MINYISMAXY: return (x, y), (x + w, y) else: return (x, y + h), (x + w, y + h) elif edge == Edge.MinY: if MINYISMAXY: return (x, y + h), (x + w, y + h) else: return (x, y), (x + w, y) elif edge == Edge.MinX: return (x, y), (x, y + h) elif edge == Edge.MaxX: return (x + w, y), (x + w, y + h) elif edge == Edge.CenterX: return (x + w/2, y), (x + w/2, y + h) elif edge == Edge.CenterY: return (x, y + h/2), (x + w, y + h/2) ================================================ FILE: packages/coldtype-core/src/coldtype/geometry/rect.py ================================================ import math, re, inspect from coldtype.geometry.geometrical import Geometrical from coldtype.geometry.point import Point from coldtype.geometry.line import Line from coldtype.geometry.edge import Edge, txt_to_edge from coldtype.interpolation import norm from coldtype.geometry.primitives import * try: from fontTools.misc.transform import Transform except ImportError: Transform = None COMMON_PAPER_SIZES = { 'letter': (612, 792), 'tabloid': (792, 1224), 'ledger': (1224, 792), 'legal': (612, 1008), 'a0': (2384, 3371), 'a1': (1685, 2384), 'a2': (1190, 1684), 'a3': (842, 1190), 'a4': (595, 842), 'a4Small': (595, 842), 'a5': (420, 595), 'b4': (729, 1032), 'b5': (516, 729), 'folio': (612, 936), 'quarto': (610, 780), '10x14': (720, 1008), } for key, (w, h) in list(COMMON_PAPER_SIZES.items()): COMMON_PAPER_SIZES["%s-landscape" % key] = (h, w) def pair_to_edges(x, y=None): if x == "NE": return Edge.MaxX, Edge.MaxY elif x == "NW": return Edge.MinX, Edge.MaxY elif x == "SW": return Edge.MinX, Edge.MinY elif x == "SE": return Edge.MaxX, Edge.MinY elif x == "N": return Edge.CenterX, Edge.MaxY elif x == "S": return Edge.CenterX, Edge.MinY elif x == "E": return Edge.MaxX, txt_to_edge(y) elif x == "W": return Edge.MinX, txt_to_edge(y) elif x == "C": return Edge.CenterX, Edge.CenterY if y is not None: return txt_to_edge(x), txt_to_edge(y) else: e = txt_to_edge(x) return e, e def align(b, rect, x=Edge.CenterX, y=Edge.CenterY, round_result=False): x, y = pair_to_edges(x, y) xoff = 0 if x != None: if x == Edge.CenterX: xoff = -b.x + rect.x + rect.w/2 - b.w/2 elif x == Edge.MinX: xoff = -(b.x-rect.x) elif x == Edge.MaxX: xoff = -b.x + rect.x + rect.w - b.w yoff = 0 if y != None: if y == Edge.CenterY: yoff = -b.y + rect.y + rect.h/2 - b.h/2 elif y == Edge.MaxY: yoff = (rect.y + rect.h) - (b.h + b.y) elif y == Edge.MinY: yoff = -(b.y-rect.y) #diff = rect.w - b.w if round_result: return round(xoff), round(yoff) else: return (xoff, yoff) class GeoIterable(): def __init__(self, *items): self.items = items def __getitem__(self, key): return self.items[key] def map(self, fn): out = [] for idx, item in enumerate(self.items): arg_count = len(inspect.signature(fn).parameters) if arg_count == 1: result = fn(item) else: result = fn(idx, item) out.append(result) return GeoIterable(*out) class Rect(Geometrical): """ Representation of a rectangle as (x, y, w, h), indexable Constructor handles multiple formats, including: * `x, y, w, h` * `[x, y, w, h]` * `w, h` (x and y default to 0, 0) `Rect` objects can be splat'd where lists are expected as individual arguments (as in drawBot), i.e. `rect(*my_rect)`, or can be passed directly to functions expected a list representation of a rectangle. """ def FromCenter(center, w, h=None) -> "Rect": """Create a rect given a center point and a width and height (optional, height will default to width if not specified")""" x, y = center if not h: h = w return Rect((x - w/2, y - h/2, w, h)) def Inches(w, h, dpi=72.0) -> "Rect": return Rect(w*dpi, h*dpi) def __init__(self, *rect): if hasattr(rect[0], "rect") and not isinstance(rect[0], Rect): rect = [rect[0].rect] if isinstance(rect[0], str): x, y = 0, 0 w, h = COMMON_PAPER_SIZES[rect[0].lower()] elif isinstance(rect[0], int) or isinstance(rect[0], float): if len(rect) == 1: x, y = 0, 0 w, h = rect[0], rect[0] else: try: x, y, w, h = rect except: w, h = rect x, y = 0, 0 else: try: x, y, w, h = rect[0] except: w, h = rect[0] x, y = 0, 0 self.x = x self.y = y self.w = w self.h = h def origin(self) -> tuple: """`(x, y)` as tuple""" return self.x, self.y def from_obj(obj, w=None, h=None) -> "Rect": r = Rect((0, 0, 0, 0)) try: r.x = obj.x r.y = obj.y except: pass try: r.w = obj.w r.h = obj.h except: pass if w: r.w = w r.x -= w/2 if h: r.h = h r.y -= h/2 return r def FromExtents(extents) -> "Rect": nw, ne, se, sw = extents return Rect(sw[0], sw[1], abs(ne[0] - sw[0]), abs(ne[1] - sw[1])) def noop(self, *args, **kwargs) -> "Rect": return self def FromMnMnMxMx(extents) -> "Rect": """Create a rectangle from `xmin, ymin, xmax, ymax`""" xmin, ymin, xmax, ymax = extents return Rect(xmin, ymin, xmax - xmin, ymax - ymin) def FromPoints(*points) -> "Rect": xmin, ymin, xmax, ymax = None, None, None, None for p in points: if xmin is None or p[0] < xmin: xmin = p[0] if ymin is None or p[1] < ymin: ymin = p[1] if xmax is None or p[0] > xmax: xmax = p[0] if ymax is None or p[1] > ymax: ymax = p[1] return Rect.FromMnMnMxMx([xmin, ymin, xmax, ymax]) def mnmnmxmx(self) -> tuple: """Return extents of rectangle as list""" return (self.x, self.y, self.x + self.w, self.y + self.h) def __getitem__(self, key): return self.rect()[key] def __repr__(self): return "Rect({:.2f},{:.2f},{:.2f},{:.2f})".format(*self.rect()) def __eq__(self, r): try: return all([self.x == r.x, self.y == r.y, self.w == r.w, self.h == r.h]) except: return False def __gt__(self, other): return self.w > other.w and self.h > other.h def __lt__(self, other): return self.w < other.w and self.h < other.h def __ge__(self, other): return self.w >= other.w and self.h >= other.h def __le__(self, other): return self.w <= other.w and self.h <= other.h __hash__ = object.__hash__ def rect(self) -> list: """x,y,w,h in list""" return [self.x, self.y, self.w, self.h] def ambit(self, tx=None, ty=None) -> "Rect": return self @property def r(self) -> "Rect": """A Scaffold has an .r, this was we can always 'cast' a Scaffold/Rect to a Rect""" return self xywh = rect def round(self) -> "Rect": """round the values in the rectangle to the nearest integer""" return Rect([int(round(n)) for n in self]) def xy(self) -> list: """equivalent to origin""" return [self.x, self.y] def wh(self) -> list: """the width and height as a tuple""" return [self.w, self.h] @property def mnx(self) -> int: return self.x @property def mny(self) -> int: return self.y @property def mxx(self) -> int: return self.x + self.w @property def mxy(self) -> int: return self.y + self.h @property def mdx(self) -> int: return self.point("C").x @property def mdy(self) -> int: return self.point("C").y def square(self, outside=False) -> "Rect": """take a square from the center of this rect""" if not outside: return Rect(centered_square_inside(self.rect())) else: return Rect(centered_square_outside(self.rect())) def fit_aspect(self, x, y, align="C", grid=False): aspect_ratio = x / y if self.w / self.h > aspect_ratio: scale = self.h / y inner_width = x * scale inner_height = self.h else: scale = self.w / x inner_width = self.w inner_height = y * scale r = Rect(self.x, self.y, inner_width, inner_height).align(self, align) if grid: return r.grid(x, y) else: return r def align(self, rect, x=Edge.CenterX, y=Edge.CenterY, round_result=False) -> "Rect": return self.offset(*align(self, rect, x, y, round_result=round_result)) def ipos(self, pt, defaults=(0.5, 0.5), clamp=True) -> tuple: """ Get scaled 0-1 bounded (optional) value from a point in a rectangle """ if not pt: return defaults sx = ((pt.x - self.x) / self.w) sy = ((pt.y - self.y) / self.h) if clamp: sx = min(1, max(0, sx)) sy = min(1, max(0, sy)) return sx, sy def divide(self, amount, edge, forcePixel=False) -> list: """ **Dividing** Derived from the behavior of the classic Cocoa function CGRectDivide, which takes a rectangle and breaks it into two pieces, based on a pixel amount and an edge. A quick example: assume you have a rectangle, `r`, defined as such: `r = Rect(0, 0, 300, 100)` If you want to break that into a left-hand rectangle that’s 100 pixels wide and a right-hand rectangle that’s 200 pixels wide, you could either say: `left, right = r.divide(100, "mnx")` `or you could say` `right, left = r.divide(200, "mxx")` where `mxx` is the rightmost edge, and `mnx` is the leftmost edge. **Centering** A special use-case is if you want to break a rectangle into `three` rectangles, based on the center "edge", you can do something like this: `left, center, right = r.divide(200, "mdx")` This will result in three rectangles, always left-to-right, where left is 50px wide, then center is 200px wide, then right is also 50px wide — anything not in the center will be evenly distributed between left and right, or top-and-bottom in the case of a Y edge. """ edge = txt_to_edge(edge) if edge == Edge.CenterX or edge == Edge.CenterY: a, b, c = divide(self.rect(), amount, edge, forcePixel=forcePixel) return GeoIterable(Rect(a), Rect(b), Rect(c)) else: a, b = divide(self.rect(), amount, edge, forcePixel=forcePixel) return GeoIterable(Rect(a), Rect(b)) def subdivide(self, amount, edge, forcePixel=False) -> list: """ Like `divide`, but here you specify the number of equal pieces you want (like columns or rows), and then what edge to start at, i.e. .. code:: python r = Rect(0, 0, 500, 100) r.subdivide(5, "mxx") => [Rect([400.0, 0, 100.0, 100]), Rect([300.0, 0, 100.0, 100]), Rect([200.0, 0, 100.0, 100]), Rect([100.0, 0, 100.0, 100]), Rect([0, 0, 100.0, 100])] will get you five 100-px wide rectangles, right-to-left (N.B. Does not support center edges, as that makes no sense) """ edge = txt_to_edge(edge) return [Rect(x) for x in subdivide(self.rect(), amount, edge, forcePixel=forcePixel)] def subdivide_with_leading(self, count, leading, edge, forcePixel=True) -> list: """ Same as `subdivide`, but inserts leading between each subdivision """ return self.subdivide_with_leadings(count, [leading]*(count-1), edge, forcePixel) def subdivide_with_leadings(self, count, leadings, edge, forcePixel=True) -> list: """ Same as `subdivide_with_leadings`, but inserts leading between each subdivision, indexing the size of the leading from a list of leadings """ edge = txt_to_edge(edge) leadings = leadings + [0] full = self.w if edge == Edge.MinX or edge == Edge.MaxX else self.h unit = (full - sum(leadings)) / count amounts = [val for pair in zip([unit] * count, leadings) for val in pair][:-1] return [Rect(x) for x in subdivide(self.rect(), amounts, edge, forcePixel=forcePixel)][::2] sub = subdivide subl = subdivide_with_leading subls = subdivide_with_leadings def transform(self, t) -> "Rect": pts = ["NW", "NE", "SE", "SW"] x1, x2, x3, x4 = [t.transformPoint(self.point(pt)) for pt in pts] return Rect.FromExtents([x1, x2, x3, x4]) def rotate(self, degrees, point=None) -> "Rect": if Transform: t = Transform() if not point: point = self.point("C") t = t.translate(point.x, point.y) t = t.rotate(math.radians(degrees)) t = t.translate(-point.x, -point.y) return self.transform(t) else: raise Exception("fontTools not installed") def scale(self, s, x_edge=Edge.MinX, y_edge=Edge.MinY) -> "Rect": return Rect(scale(self.rect(), s, x_edge, y_edge)) #x_edge = txt_to_edge(x_edge) #y_edge = txt_to_edge(y_edge) #sx = self.w * s #sy = self.h * s #return self.take(sx, x_edge, forcePixel=True).take(sy, y_edge, forcePixel=True) def union(self, otherRect) -> "Rect": return Rect.FromMnMnMxMx(unionRect(self.mnmnmxmx(), otherRect.mnmnmxmx())) def intersection(self, otherRect) -> "Rect": return Rect.FromMnMnMxMx(sectRect(self.mnmnmxmx(), otherRect.mnmnmxmx())[1]) sect = intersection def take(self, amount, edge, forcePixel=False) -> "Rect": """ Like `divide`, but here it just returns the "first" rect from a divide call, not all the resulting pieces, i.e. you can "take" 200px from the center of a rectangle by doing this `Rect(0, 0, 300, 100).take(200, "mdx")` which will result in `Rect([50, 0, 200, 100])` """ edge = txt_to_edge(edge) if not isinstance(edge, Edge): res = self for e in edge: res = res.take(amount, e, forcePixel=forcePixel) return res else: return Rect(take(self.rect(), amount, edge, forcePixel=forcePixel)) def takeOpposite(self, amount, edge, forcePixel=False) -> "Rect": edge = txt_to_edge(edge) return self.divide(amount, edge, forcePixel=forcePixel)[1] def subtract(self, amount, edge, forcePixel=False) -> "Rect": """ The opposite of `take`, this will remove and not return a piece of the given amount from the given edge. Let's say you have a 100px-wide square and you want to drop 10px from the right-hand side, you would do: `Rect(100, 100).subtract(10, Edge.MaxX)`, which leaves you with `Rect([0, 0, 90, 100])` """ edge = txt_to_edge(edge) return Rect(subtract(self.rect(), amount, edge, forcePixel=forcePixel)) drop = subtract def expand(self, amount, edge) -> "Rect": edges = None if edge == "NW": edges = ["mxy", "mnx"] elif edge == "NE": edges = ["mxy", "mxx"] elif edge == "SE": edges = ["mny", "mxx"] elif edge == "SW": edges = ["mny", "mnx"] if edges: return self.expand(amount, edges[0]).expand(amount, edges[1]) edge = txt_to_edge(edge) return Rect(expand(self.rect(), amount, edge)) add = expand def inset(self, dx, dy=None) -> "Rect": """ Creates padding in the amount of dx and dy. Also does expansion with negative values, or both at once """ if dy == None: dy = dx return Rect(inset(self.rect(), dx, dy)) def inset_x(self, dx) -> "Rect": return self.inset(dx, 0) def inset_y(self, dy) -> "Rect": return self.inset(0, dy) def offset(self, dx, dy=None) -> "Rect": if dy == None: dy = dx return Rect(offset(self.rect(), dx, dy)) def offset_x(self, dx) -> "Rect": return self.offset(dx, 0) def offset_y(self, dy) -> "Rect": return self.offset(0, dy) o = offset def zero(self) -> "Rect": """disregard origin and set it to (0,0)""" return Rect((0, 0, self.w, self.h)) def nonzero(self) -> bool: """is this rect not just all zeros?""" return not (self.x == 0 and self.y == 0 and self.w == 0 and self.h == 0) def __add__(self, another_rect): return self.union(another_rect) #return Rect(add(self, another_rect)) def grid(self, columns=2, rows=None) -> list: """Construct a grid; if rows is None, rows = columns""" if rows is None: rows = columns xs = [row.subdivide(columns, Edge.MinX) for row in self.subdivide(rows, Edge.MaxY)] return [item for sublist in xs for item in sublist] def pieces(self, amount, edge) -> list: edge = txt_to_edge(edge) return [Rect(x) for x in pieces(self.rect(), amount, edge)] def edge(self, edge) -> Line: edge = txt_to_edge(edge) return Line(*edgepoints(self.rect(), edge)) def center(self) -> Point: return Point(centerpoint(self.rect())) def flip(self, h) -> "Rect": return Rect([self.x, h - self.h - self.y, self.w, self.h]) def cardinals(self) -> tuple: return self.point("N"), self.point("E"), self.point("S"), self.point("W") def intercardinals(self) -> tuple: return self.point("NE"), self.point("SE"), self.point("SW"), self.point("NW") def FromIntercardinals(pts) -> "Rect": ne, se, sw, nw = pts return Rect(sw[0], sw[1], abs(ne[0] - sw[0]), abs(ne[1] - sw[1])) def aspect(self) -> float: return self.h / self.w def fit(self, other) -> "Rect": sx, sy, sw, sh = self ox, oy, ow, oh = other if ow > oh: fw = sw fh = sw * other.aspect() else: fh = sh sw = sh * 1/other.aspect() #print(sh, fw, fh, other.aspect()) return self.take(fh, "mdy") def avg(self) -> Point: pts = self.cardinals() return Point( sum([p.x for p in pts])/4, sum([p.y for p in pts])/4) def asciih(self, layout, areas): from coldtype.grid import Grid g = Grid(self, layout, "a", areas) out = [] for k, v in g.keyed.items(): try: ki = int(k) out.append([ki, v]) except ValueError: pass return [v[1] for v in sorted(out, key=lambda x: x[0])] def asciiv(self, layout, areas): from coldtype.grid import Grid g = Grid(self, "a", layout, areas.replace(" ", " / ")) out = [] for k, v in g.keyed.items(): try: ki = int(k) out.append([ki, v]) except ValueError: pass return [v[1] for v in sorted(out, key=lambda x: x[0])] def point(self, eh, ev=Edge.MinX) -> Point: """ Get a `Point` at a given compass direction, chosen from * C * W * NW * N * NE * E * SE * S * SW """ ev = txt_to_edge(ev) pc = Edge.PairFromCompass(eh) if pc: return self.point(*pc) else: px = self.x py = self.y if eh == Edge.MaxX: px = self.x + self.w elif eh == Edge.CenterX: px = self.x + self.w/2 if ev == Edge.MaxY: py = self.y + self.h if ev == Edge.CenterY: py = self.y + self.h/2 return Point((px, py), rect=self, corner=(eh, ev)) p = point def nsew(self): return [self.en, self.es, self.ee, self.ew] @property def pne(self) -> Point: return self.point("NE") @property def pe(self) -> Point: return self.point("E") @property def ee(self) -> Line: return self.edge("mxx") @property def pse(self) -> Point: return self.point("SE") @property def ps(self) -> Point: return self.point("S") @property def es(self) -> Line: return self.edge("mny") @property def psw(self) -> Point: return self.point("SW") @property def pw(self) -> Point: return self.point("W") @property def ew(self) -> Line: return self.edge("mnx") @property def pnw(self) -> Point: return self.point("NW") @property def pn(self) -> Point: return self.point("N") @property def en(self) -> Line: return self.edge("mxy") @property def pc(self) -> Point: return self.point("C") @property def ecx(self) -> Line: return self.edge("mdx") @property def ecy(self) -> Line: return self.edge("mdy") def contains(self, other) -> bool: return (self.pne.x >= other.pne.x and self.pne.y >= other.pne.y and self.psw.x <= other.psw.x and self.psw.y <= other.psw.y) def __contains__(self, item) -> bool: return self.contains(item) def intersects(self, other) -> bool: return not (self.point("NE").x < other.point("SW").x or self.point("SW").x > other.point("NE").x or self.point("NE").y < other.point("SW").y or self.point("SW").y > other.point("NE").y) def maxima(self, n, edge) -> "Rect": e = txt_to_edge(edge) if e == Edge.MinX: return self.setmnx(n) elif e == Edge.MaxX: return self.setmxx(n) elif e == Edge.CenterX: return self.setmdx(n) elif e == Edge.MinY: return self.setmny(n) elif e == Edge.MaxY: return self.setmxy(n) elif e == Edge.CenterY: return self.setmdy(n) else: raise Exception("HELLO") def setmnx(self, x) -> "Rect": mnx, mny, mxx, mxy = self.mnmnmxmx() return Rect.FromMnMnMxMx([x, mny, mxx, mxy]) def __mul__(self, other): return self.setmnx(other) def setlmnx(self, x): if x > self.mnx: return self.setmnx(x) return self def setmny(self, y): mnx, mny, mxx, mxy = self.mnmnmxmx() return Rect.FromMnMnMxMx([mnx, y, mxx, mxy]) def __matmul__(self, other): return self.setmny(other) def setlmny(self, y): if y > self.mny: return self.setmny(y) return self def setmxx(self, x): mnx, mny, mxx, mxy = self.mnmnmxmx() return Rect.FromMnMnMxMx([mnx, mny, x, mxy]) def setlmxx(self, x): if x < self.mxx: return self.setmxx(x) return self def setmxy(self, y): mnx, mny, mxx, mxy = self.mnmnmxmx() return Rect.FromMnMnMxMx([mnx, mny, mxx, y]) def setlmxy(self, y): if y < self.mxy: return self.setmxy(y) return self def setmdx(self, x): c = self.point("C") return Rect.FromCenter(Point([x, c.y]), self.w, self.h) def setmn(self, mn): return self.setmnx(mn.x).setmny(mn.y) def setmx(self, mx): return self.setmxx(mx.x).setmxy(mx.y) def setw(self, w): return Rect(self.x, self.y, w, self.h) def seth(self, h): return Rect(self.x, self.y, self.w, h) def parse_line(self, d, line): parts = re.split(r"\s|ƒ|,", line) reified = [] for p in parts: if p == "auto" or p == "a": reified.append("auto") elif "%" in p: reified.append(float(p.replace("%", ""))/100 * d) else: fp = float(p) if fp > 1: reified.append(fp) else: reified.append(fp*d) remaining = d - sum([0 if r == "auto" else r for r in reified]) #if not float(remaining).is_integer(): # raise Exception("floating parse") auto_count = reified.count("auto") if auto_count > 0: auto_d = remaining / auto_count auto_ds = [auto_d] * auto_count if not auto_d.is_integer(): auto_d_floor = math.floor(auto_d) leftover = remaining - auto_d_floor * auto_count for aidx, ad in enumerate(auto_ds): if leftover > 0: auto_ds[aidx] = auto_d_floor + 1 leftover -= 1 else: auto_ds[aidx] = auto_d_floor #print(auto_ds) #print("NO", auto_d - int(auto_d)) res = [] for r in reified: if r == "auto": res.append(auto_ds.pop()) else: res.append(r) return res #return [auto_d if r == "auto" else r for r in reified] def __floordiv__(self, other): return self.offset(0, other) def __truediv__(self, other): return self.offset(other, 0) def symbol_to_edge(self, idx, symbol): d = ["x", "y"][idx] if symbol == "-": return "mn" + d elif symbol == "+": return "mx" + d elif symbol == "=": return "md" + d def sign_to_dim(self, sign): xy = "x" if isinstance(sign, complex): xy = "y" sign = sign.imag return xy, sign def sign_to_edge(self, sign): xy, sign = self.sign_to_dim(sign) if sign == 0: return "md" + xy elif sign < 0: return "mn" + xy else: return "mx" + xy def t(self, sign, n): return self.take(n, self.sign_to_edge(sign)) def s(self, sign, n): return self.subtract(n, self.sign_to_edge(sign)) def i(self, sign, n): xy, _ = self.sign_to_dim(sign) return self.inset(n if xy == "x" else 0, n if xy == "y" else 0) def columns(self, *args): r = self _xs = " ".join([str(s) for s in args]) ws = self.parse_line(r.w, _xs) rs = [] for w in ws: _r, r = r.divide(w, "mnx") rs.append(_r) return rs def rows(self, *args): r = self _xs = " ".join([str(s) for s in args]) ws = self.parse_line(r.h, _xs) rs = [] for w in ws: _r, r = r.divide(w, "mxy") rs.append(_r) return rs def interp(self, v, other) -> "Rect": """Interpolate with another rect""" apts = self.intercardinals() bpts = other.intercardinals() ipts = [p1.interp(v, p2) for p1, p2 in zip(apts, bpts)] return Rect.FromIntercardinals(ipts) def is_integer(self): return all(float(x).is_integer() for x in self.rect()); ================================================ FILE: packages/coldtype-core/src/coldtype/grid/__init__.py ================================================ import re, math from coldtype.geometry import Rect, Edge, Point from string import ascii_lowercase def parse_line(d, line): parts = re.split(r"\s", line.strip()) reified = [] for p in parts: if p == "auto" or p == "a": reified.append("auto") elif "%" in p: reified.append(float(p.replace("%", ""))/100 * d) else: fp = float(p) reified.append(fp) remaining = d - sum([0 if r == "auto" else r for r in reified]) if not float(remaining).is_integer(): remaining = round(remaining) #raise Exception("floating parse") auto_count = reified.count("auto") if auto_count > 0: auto_d = remaining / auto_count auto_ds = [auto_d] * auto_count if not auto_d.is_integer(): auto_d_floor = math.floor(auto_d) leftover = remaining - auto_d_floor * auto_count for aidx, ad in enumerate(auto_ds): if leftover > 0: auto_ds[aidx] = auto_d_floor + 1 leftover -= 1 else: auto_ds[aidx] = auto_d_floor #print(auto_ds) #print("NO", auto_d - int(auto_d)) res = [] for r in reified: if r == "auto": res.append(auto_ds.pop()) else: res.append(r) return res #return [auto_d if r == "auto" else r for r in reified] def union_rect(r1, r2): ox = min(r1.x, r2.x) oy = min(r1.y, r2.y) ex = max(r1.x+r1.w, r2.x+r2.w) ey = max(r1.y+r1.h, r2.y+r2.h) return Rect(ox, oy, ex-ox, ey-oy) class Grid(): def __init__(self, r, columns="auto", rows="auto", areas=None, warn_float=True, forcePixel=False, ): self._rect = r self.warn_float = warn_float self.force_pixel = forcePixel if isinstance(columns, str): self.columns = columns else: self.columns = ("a "*columns).strip() if isinstance(rows, str): self.rows = rows else: self.rows = ("a "*rows).strip() _rows = self.rows.replace("|", "").replace(" ", " ").split(" ") _columns = self.columns.replace("|", "").replace(" ", " ").split(" ") self.areas = areas if not self.areas: ys = ascii_lowercase xs = [f"{i}" for i in range(1, 100)] cs = [] cw = len(_columns) if len(_rows) == 1: cs = [" ".join([f"a{i}" for i in xs[0:cw]])] else: for ridx, r in enumerate(_rows): pre = ys[ridx] cs.append(pre + f" {pre}".join(xs[0:cw])) self.areas = " / ".join(cs) self.cells = None self.keyed = None self.update() def __repr__(self): return f"Grid({list(self.keyed.keys())})" def clone(self, other_grid): self.columns = other_grid.columns self.rows = other_grid.rows self.areas = other_grid.areas return self @property def r(self): return self.rect @property def rect(self): return self._rect @rect.setter def rect(self, rect): self._rect = rect self.update() return self def key(self): if isinstance(self._rect, str): return self._rect def update(self): if self.key(): return # TODO should be read from a configurable dict # (on a subclass of grid?) self.columns = self.columns.replace("$CLH", "36") self.rows = self.rows.replace("$CLH", "36") cg, bs = self.calc_grid(self._rect, self.columns, self.rows, self.areas) self.borders = bs if self.areas: self.keyed = cg self.cells = None else: self.cells = cg self.keyed = None def __getitem__(self, key): if self.cells: if isinstance(key, int): return self.cells[key] else: raise Exception("Must query cell-grid with indices") elif self.keyed: if isinstance(key, str): return self.keyed[key] else: print(">>>>>>>>>>", self, key) raise Exception("Must query area-grid with strings") def calc_grid(self, r, columns, rows, areas): cs = parse_line(r.w, columns) rs = parse_line(r.h, rows) if areas: areas = [a.strip().split(" ") for a in areas.split("/")] _grid = [] keyed = {} borders = [] for idx, rr in enumerate(r.subdivide(rs[:-1], "mxy", forcePixel=self.force_pixel)): cells = rr.subdivide(cs[:-1], "mnx", forcePixel=self.force_pixel) _grid.extend(cells) if areas: _keyed = {} last_area = None jdx = 0 if idx >= len(areas): continue for ra in areas[idx]: if not ra: continue if ra == "||": borders.append([last_area, Edge.MaxX, 1]) continue elif ra == "|": borders.append([last_area, Edge.MaxX, 0]) continue elif ra == "__": borders.append([rr, Edge.MinY, 1]) continue elif ra == "_": borders.append([rr, Edge.MinY, 0]) continue _ra = ra.replace("_", "") if ra.startswith("__"): borders.append([_ra, Edge.MaxY, 1]) elif ra.startswith("_"): borders.append([_ra, Edge.MaxY, 0]) elif ra.endswith("__"): borders.append([_ra, Edge.MinY, 1]) elif ra.endswith("_"): borders.append([_ra, Edge.MinY, 0]) ra = _ra if last_area and last_area == ra: _keyed[ra] = _keyed[ra] = union_rect(_keyed[ra], cells[jdx]) else: try: _keyed[ra] = cells[jdx] except IndexError: print("-------------------") print(self.columns) print(self.rows) print(self.areas) print(ra) raise Exception("Invalid") last_area = ra jdx += 1 for k, v in _keyed.items(): if k not in keyed: keyed[k] = v else: keyed[k] = union_rect(keyed[k], v) if areas: if borders: b = [] for a, edge, weight in borders: if isinstance(a, Rect): b.append([a, edge, weight]) else: b.append([keyed[a], edge, weight]) else: b = [] if True: for k, v in keyed.items(): x, y, w, h = v if self.warn_float: if (not float(x).is_integer() or not float(y).is_integer() or not float(w).is_integer() or not float(h).is_integer()): print(">>> FLOAT RECT:::", k, v, "///", r) return keyed, b else: return _grid, [] ================================================ FILE: packages/coldtype-core/src/coldtype/helpers.py ================================================ from pathlib import Path from coldtype.text.reader import normalize_font_path from coldtype.interpolation import norm, interp_dict, lerp, loopidx from coldtype.random import random_series try: from defcon import Font as DefconFont except ImportError: DefconFont = None def sibling(root, file): return Path(root).parent.joinpath(file) def download(url, save_to:Path, force=False): import requests if not force and save_to.exists(): return save_to save_to.parent.mkdir(parents=True, exist_ok=True) response = requests.get(url) response.raise_for_status() with save_to.open("wb") as f: f.write(response.content) return save_to def raw_ufo(path): return DefconFont(normalize_font_path(path)) def quick_ufo(path , familyName , styleName="Regular" , versionMajor=1 , versionMinor=0 , unitsPerEm=1000 , descender=-250 , ascender=750 , capHeight=750 , xHeight=500 ): np:Path = Path(path).expanduser().resolve() if not np.exists(): np.parent.mkdir(exist_ok=True, parents=True) ufo = DefconFont() ufo.save(str(np)) ufo = DefconFont(str(np)) ufo.info.familyName = familyName ufo.info.styleName = styleName ufo.info.versionMajor = versionMajor ufo.info.versionMinor = versionMinor ufo.info.unitsPerEm = unitsPerEm ufo.info.descender = descender ufo.info.xHeight = xHeight ufo.info.capHeight = capHeight ufo.info.ascender = ascender return ufo def ßhide(el): return None def ßshow(el): return el def cycle_idx(arr, idx): if idx < 0: return len(arr) - 1 elif idx >= len(arr): return 0 else: return idx _by_uni = None _by_glyph = None _class_lookup = None def _populate_glyphs_unis(): global _by_uni global _by_glyph global _class_lookup _by_uni = {} _by_glyph = {} _class_lookup = {} #try: if True: lines = (Path(__file__).parent / "assets/glyphNamesToUnicode.txt").read_text().split("\n") for l in lines: if l.startswith("#"): continue l = l.split(" ")[:3] uni = int(l[1], 16) _by_uni[uni] = l[0] _by_glyph[l[0]] = uni _class_lookup[l[0]] = l[2] #except: # pass def uni_to_glyph(u): if not _by_uni: _populate_glyphs_unis() return _by_uni.get(u) def glyph_to_uni(g): if g.lower() in [ "gcommaaccent", "kcommaaccent", "lcommaaccent", "ncommaaccent", "rcommaaccent", ]: g = g.replace("commaaccent", "cedilla") elif g.lower() == "kgreenlandic": g = g.replace("greenlandic", "ra") if not _by_glyph: _populate_glyphs_unis() return _by_glyph.get(g) def glyph_to_class(g): if not _class_lookup: _populate_glyphs_unis() return _class_lookup.get(g) # Function for running async code synchronously from both standard sync code and from a notebook context #https://stackoverflow.com/questions/55647753/call-async-function-from-sync-function-while-the-synchronous-function-continues import asyncio import threading from concurrent.futures import ThreadPoolExecutor from typing import Any, Coroutine, TypeVar __all__ = [ "run_coroutine_sync", ] T = TypeVar("T") def run_coroutine_sync(coroutine: Coroutine[Any, Any, T], timeout: float = 30) -> T: def run_in_new_loop(): new_loop = asyncio.new_event_loop() asyncio.set_event_loop(new_loop) try: return new_loop.run_until_complete(coroutine) finally: new_loop.close() try: loop = asyncio.get_running_loop() except RuntimeError: return asyncio.run(coroutine) if threading.current_thread() is threading.main_thread(): if not loop.is_running(): return loop.run_until_complete(coroutine) else: with ThreadPoolExecutor() as pool: future = pool.submit(run_in_new_loop) return future.result(timeout=timeout) else: return asyncio.run_coroutine_threadsafe(coroutine, loop).result() ================================================ FILE: packages/coldtype-core/src/coldtype/img/abstract.py ================================================ from pathlib import Path from coldtype.runon.path import P from coldtype.geometry import Rect from coldtype.img.blendmode import BlendMode class AbstractImage(P): def __init__(self, src, img=None): if isinstance(src, Path) or isinstance(src, str): self.src = Path(str(src)).expanduser().absolute() if not self.src.exists(): raise Exception("Image src does not exist", self.src) if img: self._img = img else: self._img = self.load_image(self.src) else: self.src = None self._img = src self.transforms = [] self.visible = True self.alpha = 1 super().__init__() self.data(frame=self.rect()) def load_image(self, src): raise NotImplementedError() def write(self, path): raise NotImplementedError() def rect(self): return Rect(self.width(), self.height()) def bounds(self): return self.data("frame") def img(self): return None def a(self, alpha=None): if alpha is None: return self.alpha else: self.alpha = alpha return self def width(self): raise NotImplementedError() def height(self): raise NotImplementedError() def align(self, rect, x="mdx", y="mdy", round_result=True, tx=True, ty=True): """ tx and ty are here for keyword compatibility, they don't do anything """ self.data(frame=self.rect().align(rect, x, y, round_result=round_result)) return self def _resize(self, fx, fy): raise NotImplementedError() def resize(self, factor, factor_y=None): fx, fy = factor, factor if factor_y is not None: fy = factor_y if fx == 1 and fy == 1: return self self._resize(fx, fy) self.data(frame= self.rect().align(self.data("frame"), "mnx", "mny")) return self def rotate(self, degrees, point=None): self.transforms.append(["rotate", degrees, point or self.data("frame").pc]) return self def matrix(self, a, b, c, d, e, f): self.transforms.append(["matrix", [a, b, c, d, e, f]]) return self def _precompose_fn(self): raise NotImplementedError() def precompose(self, rect, as_image=True): res = P([self]).ch(self._precompose_fn()(rect)) if as_image: return type(self).FromPen(res, original_src=self.src) else: return res def crop(self, crop, mutate=True): if callable(crop): crop = crop(self) xo, yo = -crop.bounds().x, -crop.bounds().y cropped = P([ (self.in_pen().translate(xo, yo)), (P() .rect(self.bounds()) .difference(crop) .f(-1) .blendmode(BlendMode.Clear) .translate(xo, yo)) ]).ch(self._precompose_fn()( crop.bounds().zero())) if mutate: self._img = cropped.img().get("src") self.data(frame=self.rect()) return self else: return AbstractImage(self.src, img=cropped.img().get("src")) #return self def in_pen(self): return (P(self.bounds()) .img(self._img, self.bounds(), pattern=False)) def to_pen(self, rect=None): return self.precompose(rect or self.data("frame"), as_image=False) def FromPen(pen:P, original_src=None): return AbstractImage(original_src, img=pen.img().get("src")) def __str__(self): if self.src: try: return f"" except ValueError: return f"" else: return f"" ================================================ FILE: packages/coldtype-core/src/coldtype/img/blendmode.py ================================================ from enum import Enum, auto try: import skia except ImportError: skia = None class BlendMode(Enum): Clear = auto() Src = auto() Dst = auto() SrcOver = auto() DstOver = auto() SrcIn = auto() DstIn = auto() SrcOut = auto() DstOut = auto() SrcATop = auto() DstATop = auto() Xor = auto() Plus = auto() Modulate = auto() Screen = auto() Overlay = auto() Darken = auto() Lighten = auto() ColorDodge = auto() ColorBurn = auto() HardLight = auto() SoftLight = auto() Difference = auto() Exclusion = auto() Multiply = auto() Hue = auto() Saturation = auto() Color = auto() Luminosity = auto() def to_skia(self): return _SKIA_MAPPING[self.value-1] def print(self): print(self.name) return self @staticmethod def Cycle(i, show=False): bms = list(BlendMode) match = bms[i%len(bms)] if show: print(match) return match if not skia: _SKIA_MAPPING = [] else: _SKIA_MAPPING = [ skia.BlendMode.kClear, skia.BlendMode.kSrc, skia.BlendMode.kDst, skia.BlendMode.kSrcOver, skia.BlendMode.kDstOver, skia.BlendMode.kSrcIn, skia.BlendMode.kDstIn, skia.BlendMode.kSrcOut, skia.BlendMode.kDstOut, skia.BlendMode.kSrcATop, skia.BlendMode.kDstATop, skia.BlendMode.kXor, skia.BlendMode.kPlus, skia.BlendMode.kModulate, skia.BlendMode.kScreen, skia.BlendMode.kOverlay, skia.BlendMode.kDarken, skia.BlendMode.kLighten, skia.BlendMode.kColorDodge, skia.BlendMode.kColorBurn, skia.BlendMode.kHardLight, skia.BlendMode.kSoftLight, skia.BlendMode.kDifference, skia.BlendMode.kExclusion, skia.BlendMode.kMultiply, skia.BlendMode.kHue, skia.BlendMode.kSaturation, skia.BlendMode.kColor, skia.BlendMode.kLuminosity] if __name__ == "__main__": print(BlendMode.Saturation.to_skia()) ================================================ FILE: packages/coldtype-core/src/coldtype/img/drawbotimage.py ================================================ from coldtype.img.abstract import AbstractImage try: import drawBot as db except ImportError: print("No DrawBot installed! `pip install git+https://github.com/typemytype/drawbot`") class DrawBotImage(AbstractImage): def load_image(self, src): w, h = db.imageSize(src) im = db.ImageObject() with im: db.size(w, h) db.image(src, (0, 0)) return im def width(self): return db.imageSize(self.src)[0] def height(self): return db.imageSize(self.src)[1] # def _resize(self, fx, fy): # self._img = self._img.resize( # int(self._img.width()*fx), # int(self._img.height()*fy)) # def _precompose_fn(self): # return precompose # def write(self, path): # self._img.save(str(path), skia.kPNG) # return self ================================================ FILE: packages/coldtype-core/src/coldtype/img/skiaimage.py ================================================ import skia from coldtype.img.abstract import AbstractImage from coldtype.fx.skia import precompose from coldtype.skiashim import canvas_drawImage, paint_withFilterQualityHigh, image_resize from coldtype.runon.path import P class SkiaImage(AbstractImage): @staticmethod def FromBase64(b64): import base64 if b64.startswith("data:image/"): b64 = b64.split(",")[1] image_data = base64.b64decode(b64) skia_image = skia.Image.MakeFromEncoded(skia.Data.MakeWithCopy(image_data)) return SkiaImage(skia_image) def load_image(self, src): return skia.Image.MakeFromEncoded(skia.Data.MakeFromFileName(str(src))) def width(self): return self._img.width() def height(self): return self._img.height() def copy(self): return SkiaImage(self._img) def _resize(self, fx, fy): # should preserve the offset? # or somehow keep the alignment? self._img = image_resize(self._img, round(self._img.width()*fx), round(self._img.height()*fy)) def _precompose_fn(self): return precompose def css_scale(self, x, y=None): if y is None: y = x self._img = self._redraw(lambda canvas, paint: canvas.scale(x, y)) return self def _redraw(self, modifier): from coldtype.fx.skia import SKIA_CONTEXT from coldtype.pens.skiapen import SkiaPen frame = self.data("frame") def rotator(canvas): paint = paint_withFilterQualityHigh() modifier(canvas, paint) canvas_drawImage(canvas, self._img, 0, 0, paint) return SkiaPen.Precompose(rotator, frame.inset(0), context=SKIA_CONTEXT) def css_translate(self, x, y=None): self._img = self._redraw(lambda canvas, paint: canvas.translate(x, y)) def css_matrix(self, a, b, c, d, tx, ty): matrix = skia.Matrix([a, c, tx, b, d, ty, 0, 0, 1]) self._img = self._redraw(lambda canvas, paint: canvas.setMatrix(matrix)) return self def _rotate(self, degrees, point=None): #self.transforms.append(["rotate", degrees, point or self.data("frame").pc]) from coldtype.fx.skia import SKIA_CONTEXT from coldtype.pens.skiapen import SkiaPen frame = self.data("frame") rotated = P(frame).rotate(degrees, point) rotated_frame = rotated.ambit()#.zero() dx, dy = rotated_frame.x - frame.x, rotated_frame.y - frame.y width, height = self.width(), self.height() center_x, center_y = width / 2, height / 2 def rotator(canvas): paint = paint_withFilterQualityHigh() canvas.translate(-dx, -dy) canvas.translate(center_x, center_y) canvas.rotate(-degrees) canvas.translate(-center_x, -center_y) canvas_drawImage(canvas, self._img, 0, 0, paint) img = SkiaPen.Precompose(rotator, rotated_frame, context=SKIA_CONTEXT) return SkiaImage(img)#.translate(dx, dy) def write(self, path): self._img.save(str(path), skia.kPNG) return self ================================================ FILE: packages/coldtype-core/src/coldtype/img/skiasvg.py ================================================ import skia from coldtype.img.abstract import AbstractImage class SkiaSVG(AbstractImage): def width(self): return self._img.containerSize().width() def height(self): return self._img.containerSize().height() def copy(self): return SkiaSVG(self._img) ================================================ FILE: packages/coldtype-core/src/coldtype/interpolation/__init__.py ================================================ def norm(value, start, stop): return start + (stop-start) * value def lerp(start, stop, amt): return float(amt-start) / float(stop-start) def interp_dict(v, a, b=None): if not isinstance(a, dict): return norm(v, a, b) if b is None: a, b = a[0], a[1] out = dict() for k, _v in a.items(): if hasattr(a[k], "interp"): out[k] = a[k].interp(v, b[k]) elif isinstance(a[k], str): out[k] = b[k] else: out[k] = norm(v, a[k], b[k]) return out def loopidx(lst, idx): return lst[idx % len(lst)] ================================================ FILE: packages/coldtype-core/src/coldtype/midi/controllers.py ================================================ def _norm(lookup, k, default): return lookup.get(str(k), 63.5 if default == None else default*127) / 127 def midi_controller_lookup_fn(name, column_starts=[], cmc={}, channel="9"): """ Intuitive lookup function generator for midi controls scheme is bottom-to-top, 10 is equivalent to column 1, row 0 22 == column 2, row 3, etc. (or can be customized for whatever device you’d like) """ # TODO use the name so individual devices are targetable def lookup(ctrl, default=None, relative=False): scoped = cmc.get(name, {}) scoped = scoped.get(str(channel), {}) if column_starts: column = int(str(ctrl)[0]) row = int(str(ctrl)[1]) mnum = column_starts[row]+(column-1) else: mnum = ctrl current = _norm(scoped, mnum, default) if relative: was = _norm(scoped, "_"+str(mnum), default) rel = current - was return rel else: return current return lookup def Generic(name, cmc, channel): return midi_controller_lookup_fn(name, cmc=cmc, channel=channel) def LaunchControlXL(cmc, channel="9"): return midi_controller_lookup_fn( "Launch Control XL", column_starts=[77, 49, 29, 13], cmc=cmc, channel=channel) def LaunchkeyMini(cmc, channel="9"): return midi_controller_lookup_fn( "Launchkey Mini LK Mini MIDI", column_starts=[21], cmc=cmc, channel=channel) ================================================ FILE: packages/coldtype-core/src/coldtype/notebook/__init__.py ================================================ from coldtype import * import json, os from pathlib import Path from base64 import b64encode from IPython.display import display, SVG, HTML, clear_output from coldtype.renderable.renderable import renderable as _renderable from coldtype.renderable.animation import animation as _animation, aframe as _aframe from coldtype.renderable.animation import Action, Timeline, FFMPEGExport from coldtype.runon.path import P from coldtype.pens.svgpen import SVGPen from coldtype.color import rgb, hsl from coldtype.geometry import Rect from subprocess import run from shutil import rmtree DEFAULT_DISPLAY = "png" try: from coldtype.fx.skia import precompose from coldtype.pens.skiapen import SkiaPen from io import BytesIO except ImportError: precompose = None def update_ffmpeg(): print("fetching...") os.system("add-apt-repository -y ppa:jonathonf/ffmpeg-4") print("updating...") os.system("apt-get update") print("mediainfo...") os.system("apt install mediainfo") print("ffmpeg...") os.system("apt-get install ffmpeg") clear_output() print('ffmpeg update finished') def show(fmt="png", rect=None, align=False, padding=[0, 0], tx=0, ty=0, scale=0.5): if not precompose and fmt == "png": raise Exception("pip install skia-python") def _display(_pen:P): pen = _pen.copy(with_data=1) pen.data(_notebook_shown=True) nonlocal rect, fmt if fmt is None: img = pen.img() if img and img.get("src"): fmt = "png" else: fmt = "svg" if align and rect is not None: pen.align(rect) if rect is None: lar = pen.data("_last_align_rect") if lar and False: rect = lar pen = P([P(rect).fssw(-1, 0.75, 2), pen]) else: amb = pen.ambit(tx=tx, ty=ty) rect = Rect(amb.w+padding[0], amb.h+padding[1]) pen.align(rect) if fmt == "png": src = pen.ch(precompose(rect)).img().get("src") with BytesIO(src.encodeToData()) as f: f.seek(0) # necessary? b64 = b64encode(f.read()).decode("utf-8") display(HTML(f"")) elif fmt == "svg": svg = SVGPen.Composite(pen, rect, viewBox=False) display(SVG(svg)) return _pen return _display def showpng(rect=None, align=False, padding=[60, 50], tx=0, ty=0, scale=0.5): return show("png", rect, align, padding, tx, ty, scale) def showlocalpng(rect, src, scale=0.5): with open(src, "rb") as img_file: encoded_string = b64encode(img_file.read()).decode("utf-8") display(HTML(f"")) def show_frame(a, idx, scale=0.5): rp = a.passes(Action.PreviewIndices, None, [idx])[0] res = a.run_normal(rp) show(a.fmt, a.rect, padding=[0, 0], scale=scale)(res) #rp.output_path.parent.mkdir(parents=True, exist_ok=True) #SkiaPen.Composite(res, a.rect, str(rp.output_path)) #showlocalpng(a.rect, rp.output_path) js = """ function animate(name, start_playing) { svg = document.querySelector(`#${name} svg`); svg_frames = [].slice.apply(svg.querySelectorAll(".frame")); svg_frames.forEach((sf) => sf.style.display = "none"); svg_frames[0].style.display = "block"; var fps = parseFloat(svg.dataset.fps); var duration = parseInt(svg.dataset.duration); var fpsInterval = 1000 / fps; var then = Date.now(); var startTime = then; var elapsed = 0; var i = 0; var playing = !!start_playing; function _animate() { if (!playing) { return; } requestAnimationFrame(_animate); var now = Date.now(); elapsed = now - then; if (elapsed > fpsInterval) { then = now - (elapsed % fpsInterval); svg_frames.forEach((sf) => sf.style.display = "none"); svg_frames[i%duration].style.display = "block"; i++; } } svg.addEventListener("click", function() { if (playing) { playing = false; } else { playing = true; _animate(); } }); if (playing) { _animate(); } } """ def show_animation(a:_animation, start=False): idxs = range(0, a.duration+1) passes = a.passes(Action.PreviewIndices, None, idxs) results = [a.run_normal(rp) for rp in passes] svg = SVGPen.Animation(results, a.rect, a.timeline.fps) html = f"
{svg}
" js_call = f""; display(HTML(html + js_call), display_id=a.name) def render_animation(a, show=[], preview_scale=0.5, scale=1): try: from tqdm.notebook import tqdm except (ImportError, ModuleNotFoundError): tqdm = None print("no tqdm") idxs = list(range(0, a.duration)) passes = a.passes(Action.PreviewIndices, None, idxs) output_dir = passes[0].output_path.parent if output_dir.exists(): rmtree(output_dir) output_dir.mkdir(parents=True, exist_ok=True) xs = passes if tqdm: xs = tqdm(passes, leave=False) for idx, rp in enumerate(xs): res = a.run_normal(rp) if a.fmt == "png": SkiaPen.Composite(res, a.rect, str(rp.output_path), scale=scale) if show == "*" or idx in show: showlocalpng(a.rect, rp.output_path, scale=preview_scale) elif a.fmt == "svg": SkiaPen.SVG(res, a.rect, str(rp.output_path), scale=scale) clear_output() def show_video(a, fmt="h264", loops=1, verbose=False, download=False, scale=0.5, audio=None, audio_loops=None,autoplay=True): ffex = FFMPEGExport(a, loops=loops, audio=audio, audio_loops=audio_loops) if fmt == "h264": ffex.h264() elif fmt == "gif": ffex.gif() else: print("Unrecognized fmt:", fmt) return ffex.write(verbose=verbose) compressed_path = str(ffex.output_path.absolute()) if fmt == "h264": mp4 = open(compressed_path, 'rb').read() data_url = "data:video/mp4;base64," + b64encode(mp4).decode() clear_output() display(HTML(f""" """ % data_url)) elif fmt == "gif": gif = open(compressed_path, 'rb').read() data_url = "data:image/gif;base64," + b64encode(gif).decode() clear_output() #display(HTML(f"")) display(HTML(f"""""" % data_url)) if download: try: from google.colab import files files.download(ffex.output_path) except ImportError: print("download= arg is for colab") pass class notebook_renderable(_renderable): def __init__(self, rect=(540, 540), preview=True, preview_scale=0.5, render_bg=True, border=(0.75, 2), **kwargs ): self._preview = preview self.preview_scale = preview_scale self.border = border super().__init__(rect, render_bg=render_bg, **kwargs) def __call__(self, func): res = super().__call__(func) if self._preview: self.preview() return res def preview(self): res = self.frame_result(0, post=False) out = P([ P(self.rect).fssw(-1, *self.border) if self.border else None, res ]) out.ch(show("png", self.rect, padding=[0, 0], scale=self.preview_scale)) return self class notebook_animation(_animation): def __init__(self, rect=(1080, 1080), display=True, preview_scale=0.5, render_bg=True, storyboard=None, interactive=True, render_show=False, vars={}, **kwargs ): self._display = display self.interactive = interactive self.preview_scale = preview_scale self.render_show = render_show self.vars = vars if storyboard is None: self._storyboarded = False storyboard = [0] else: self.interactive = False self._storyboarded = True self.storyboard = storyboard super().__init__(rect, render_bg=render_bg, **kwargs) def __call__(self, func): res = super().__call__(func) if self.render_show: self.render().show() elif self._display: self.display() return res def display(self): if not self.interactive: self.preview(*self.storyboard) return self._interaction_file = Path("_coldtype_notebook_tmp/" + self.name + "_tmp_state.json") self._interaction_state = {} if not self._interaction_file.exists(): self._interaction_file.parent.mkdir(parents=True, exist_ok=True) self._interaction_file.write_text("{}") if len(self.storyboard) > 1: self.preview(*self.storyboard[1:]) else: self.interactive_preview(self.storyboard[0]) def interactive_preview(self, start): from ipywidgets import IntSlider, FloatSlider, interact self._interaction_state = json.loads(self._interaction_file.read_text()) def show_anim(**kwargs): self._interaction_state = kwargs self._interaction_file.write_text(json.dumps(kwargs)) self.preview(kwargs["i"]) vars = dict(i=IntSlider( min=0, max=self.duration-1, continuous_update=False, value=self._interaction_state.get("i", 0), description="f.i")) for v, val in self.vars.items(): vars[v] = FloatSlider(min=0, max=1, step=1e-2, continuous_update=False, value=self._interaction_state.get(v, val)) interact(show_anim, **vars) def iv(self, k): return self._interaction_state.get(k, 0) def preview(self, *frames): if len(frames) == 0: frames = [0] elif len(frames) == 1: if frames[0] == "*": frames = list(range(self.start, self.end)) for frame in frames: show_frame(self, frame, scale=self.preview_scale) return self def render(self, scale=1): render_animation(self, show=[], scale=scale) return self def show(self, fmt="h264", loops=1, verbose=False, download=False, scale=0.5, audio=None, audio_loops=None, autoplay=True ): if self.fmt == "svg": show_animation(self, start=False) else: show_video(self, fmt=fmt, loops=loops, verbose=verbose, download=download, scale=scale, audio=audio, audio_loops=audio_loops, autoplay=autoplay) return None def zip(self, download=False): zipfile = f"{str(self.output_folder)}.zip" run(["zip", "-j", "-r", zipfile, str(self.output_folder)]) print("> zipped:", zipfile) if download: try: from google.colab import files files.download(zipfile) except ImportError: print("download= arg is for colab") pass return self class notebook_aframe(_aframe): def __init__(self, rect=(1080, 1080), **kwargs ): super().__init__(rect, timeline=Timeline(1), interactive=False, #storyboard=[0], **kwargs) def nshow(self): return self.ch(show(tx=1, ty=1)) P.nshow = nshow renderable = notebook_renderable animation = notebook_animation aframe = notebook_aframe # to set up font paths correctly (seems fine?) from coldtype.renderer.reader import SourceReader NOTEBOOK_SOURCE_READER = SourceReader() NOTEBOOK_SOURCE_READER.read_configs(None, None) clear_output() ================================================ FILE: packages/coldtype-core/src/coldtype/notebook/parser.py ================================================ from coldtype import * import json, dateparser, markdown, jinja2 from pygments import highlight from pygments.lexers import PythonLexer from pygments.formatters import HtmlFormatter from lxml.html import fragment_fromstring, tostring from time import time from datetime import datetime from email.utils import format_datetime from shutil import copytree from coldtype.runon import Runon class NotebookParser(): def __init__(self , notebook_dir:Path , build_dir:Path , templates_dir:Path , assets_dir:Path , do_nest:bool = False , sort:dict = {} ) -> None: self.notebook_dir = notebook_dir self.build_dir = build_dir self.templates_dir = templates_dir self.assets_dir = assets_dir self.build_dir.mkdir(exist_ok=True) # TODO recursive glob to get a tree, to make a table-of-contents instead of only flat-list self.notebooks = list(self.notebook_dir.glob("**/*.ipynb")) print(">>>", self.notebooks) self.template_env = jinja2.Environment(loader=jinja2.FileSystemLoader(str(self.templates_dir))) self.index_template = self.template_env.get_template("index.j2") self.post_template = self.template_env.get_template("post.j2") try: self.feed_template = self.template_env.get_template("feed.j2") except: self.feed_template = None posts = [] self.build_unix = int(time()) self.build_time = format_datetime(datetime.utcfromtimestamp(int(time()))).strip() for notebook in self.notebooks: if notebook.stem.startswith("_"): continue path = notebook.relative_to(self.notebook_dir) data = json.loads(Path(notebook).read_text()) frontmatter = eval("".join(data["cells"][0]["source"])) slug = frontmatter.get("slug", ".".join(str(path).split(".")[:-1])) date = dateparser.parse(frontmatter["date"]) date_string = date.strftime("%B %d, %Y") cells = [] for c in data["cells"][1:]: ct = c["cell_type"] if ct == "markdown": cells.append(dict(text=markdown.markdown("".join(c["source"]), extensions=["smarty", "fenced_code"]))) elif ct == "code": src = "".join(c["source"]) if src.strip().startswith("#hide-publish") or src.strip().startswith("#hide-blog"): continue lines = [] for line in src.splitlines(): if not line.strip().endswith("#hide-publish") and not line.strip().endswith("#hide-blog"): lines.append(line) src = "\n".join(lines) highlit = fragment_fromstring( highlight(src, PythonLexer(), HtmlFormatter(linenos=False))) html = tostring(highlit, pretty_print=True, encoding="utf-8").decode("utf-8") cell = dict(html=html) outputs = [] if "outputs" in c: for o in c["outputs"]: if o["output_type"] == "display_data" and o["data"] and "text/html" in o["data"]: outputs.append(o["data"]["text/html"]) #print(o["data"]["text/html"][:10]) elif o.get("name") == "stdout": outputs.append(["
" + "".join(o["text"]) + "
"]) #print(outputs[-1][10]) else: pass if len(outputs) > 0: cell["outputs"] = outputs else: cell["no-outputs"] = True cells.append(cell) # need to be nested somehow post = dict( metadata={ **frontmatter, "slug": slug, **dict( path=notebook, pubDate=format_datetime(date).strip(), date_string=date_string, date=date)}, notebook=notebook.stem, cells=cells) dst = self.build_dir / f"{slug}.html" dst.parent.mkdir(parents=True, exist_ok=True) dst.write_text(self.post_template.render(dict(post=post, build_unix=self.build_unix))) posts.append(post) posts = list(reversed(sorted(posts, key=lambda p: p["metadata"]["date"]))) if do_nest: class Post(Runon): def __init__(self, val): super().__init__(None) self._val = val nested_posts = Runon() for p in posts: slug = p["metadata"]["slug"] slugs = slug.split("/")[:-1] nested = nested_posts for s in slugs: n = nested.find_(s, none_ok=True) if not n: n = Runon().tag(s) nested.append(n) nested = n nested.append(Post(p).tag(slug).data(post=True)) def sorter(el, pos, data): if pos != 0: sorter = sort[el.tag()] def sortfn(x): return sorter.index(x.tag().split("/")[-1]) el._els = list(sorted(el._els, key=sortfn)) nested_posts.prewalk(sorter) posts = nested_posts copytree(self.assets_dir, self.build_dir / "assets", dirs_exist_ok=True) (self.build_dir / "index.html").write_text(self.index_template.render(dict(posts=posts, build_unix=self.build_unix))) if self.feed_template: (self.build_dir / "feed.xml").write_text(self.feed_template.render(dict(posts=posts, build=self.build_time, build_unix=self.build_unix))) ================================================ FILE: packages/coldtype-core/src/coldtype/osutil.py ================================================ import platform, os, subprocess from enum import Enum class System(Enum): Darwin = "Darwin" Windows = "Windows" Linux = "Linux" def operating_system(): sys = platform.system() if sys == "Darwin": return System.Darwin elif sys == "Windows": return System.Windows elif sys == "Linux": return System.Linux def on_windows(): return operating_system() == System.Windows def on_mac(): return operating_system() == System.Darwin def on_linux(): return operating_system() == System.Linux def play_sound(name="Pop"): """ easy sound-playing utility should be implemented on other platforms as well """ if on_mac(): os.system(f"afplay /System/Library/Sounds/{name}.aiff &") def show_in_finder(path): from showinfm import show_in_file_manager p = path.expanduser().resolve() show_in_file_manager(str(p)) # if on_mac() or on_linux(): # os.system(f"open {p}") # elif on_windows(): # os.system(f"explorer {p}") # else: # print("show-in-finder not implemented for os") def in_notebook() -> bool: try: shell = get_ipython().__class__.__name__ if shell == 'ZMQInteractiveShell': return True # Jupyter notebook or qtconsole elif shell == 'TerminalInteractiveShell': return False # Terminal running IPython else: return False # Other type (?) except NameError: return False # Probably standard Python interpreter def run_with_check(args): print("---") print("$", " ".join([str(s) for s in args])) try: subprocess.run(args, capture_output=True, check=True) except subprocess.CalledProcessError as e: print(e.stderr.decode("utf-8")) ================================================ FILE: packages/coldtype-core/src/coldtype/pens/axidrawpen.py ================================================ import time, math from fontTools.pens.basePen import BasePen from fontTools.pens.transformPen import TransformPen from coldtype.geometry import Rect, Point try: from pyaxidraw import axidraw except: print("Couldn’t import pyaxidraw") pass class AxiDrawPen(BasePen): def __init__(self, dat, page, move_delay=0): super().__init__(None) self.dat = dat self.page = page self.ad = None self.move_delay = move_delay #dat.replay(self) self.last_moveTo = None def _moveTo(self, p): self.last_moveTo = p time.sleep(self.move_delay) self.ad.moveto(*p) time.sleep(self.move_delay) def _lineTo(self, p): time.sleep(self.move_delay) self.ad.lineto(*p) def _curveToOne(self, p1, p2, p3): print("! CANNOT CURVE !") def _qCurveToOne(self, p1, p2): print("! CANNOT CURVE !") def _closePath(self): # can this work? if self.last_moveTo: self.ad.lineto(*self.last_moveTo) def draw(self, scale=0.01, cm=False, ad=None, move_delay=0, zero=True, ): self.dat.scale(scale, point=Point(0, 0)) page = self.page.scale(scale) bounds = self.dat.bounds() limits = Rect(0, 0, 11, 8.5) if cm: limits = limits.scale(2.54) def small_enough(r): return (r.mnx >= 0 and r.mny >= 0 and r.mxx <= limits.w and r.mxy <= limits.h) if small_enough(page) and small_enough(bounds): print("Drawing!") else: print("Too big!", page, bounds) return False own_ad = False if not ad: own_ad = True ad = axidraw.AxiDraw() ad.interactive() ad.options.units = 1 if cm else 0 ad.options.speed_pendown = 50 ad.options.speed_penup = 50 ad.options.pen_rate_raise = 50 if own_ad: ad.connect() ad.penup() self.ad = ad self.move_delay = move_delay tp = TransformPen(self, (1, 0, 0, -1, 0, page.h)) self.dat.replay(tp) ad.penup() time.sleep(move_delay) ad.penup() if zero: ad.moveto(0,0) if own_ad: ad.disconnect() ================================================ FILE: packages/coldtype-core/src/coldtype/pens/blenderpen.py ================================================ from contextlib import contextmanager from coldtype.geometry import Rect, Edge, Point from coldtype.beziers import CurveCutter, raise_quadratic from coldtype.pens.drawablepen import DrawablePenMixin from fontTools.pens.basePen import BasePen from coldtype.color import Gradient, Color, normalize_color from coldtype.blender.fluent import BpyObj import math import random try: # Blender-specific things import bpy from mathutils import Vector, Matrix except: #print(">>> Not a blender environment") pass class BPH(): def Clear(): print(">>>CLERAING") for block in bpy.data.meshes: if block.users == 0: bpy.data.meshes.remove(block) for block in bpy.data.materials: if block.users == 0: bpy.data.materials.remove(block) for block in bpy.data.textures: if block.users == 0: bpy.data.textures.remove(block) for block in bpy.data.images: if block.users == 0: bpy.data.images.remove(block) def FindCollectionForItem(item): collections = item.users_collection if len(collections) > 0: return collections[0] return bpy.context.scene.collection def Collection(name, parent=None): if name not in bpy.data.collections: coll = bpy.data.collections.new(name) if parent: parent.children.link(coll) else: bpy.context.scene.collection.children.link(coll) return bpy.data.collections.get(name) def CheckExists(name, dn=False): if dn and name in bpy.context.scene.objects: obj = bpy.context.scene.objects[name] bpy.data.objects.remove(obj, do_unlink=True) return False return name in bpy.context.scene.objects def AddOrFind(name, add_fn, dn=False): if dn and name in bpy.context.scene.objects: obj = bpy.context.scene.objects[name] bpy.data.objects.remove(obj, do_unlink=True) if name not in bpy.context.scene.objects: add_fn() bc = bpy.context.active_object bc.name = name return bc else: return bpy.context.scene.objects[name] def Primitive(_type, coll, name, dn=False, container=None, material="ColdtypeDefault", cyclic=True): created = False if dn: #and name in bpy.context.scene.objects: # obj = bpy.context.scene.objects[name] # bpy.data.objects.remove(obj) for m in bpy.data.objects: if name in m.name: bpy.data.objects.remove(m) for m in bpy.data.meshes: if name in m.name: bpy.data.meshes.remove(m) for m in bpy.data.materials: if name in m.name: bpy.data.materials.remove(m) if name not in bpy.context.scene.objects: created = True if _type == "Bezier": bpy.ops.curve.primitive_bezier_curve_add() elif _type == "plane": bpy.ops.mesh.primitive_plane_add() elif _type == "cube": bpy.ops.mesh.primitive_cube_add() bc = bpy.context.object bc.name = name bc.data.name = name #name = bc.name if _type == "Bezier": if cyclic: bc.data.dimensions = "2D" bc.data.fill_mode = "BOTH" bc.data.extrude = 0.1 else: bc.data.dimensions = "3D" elif _type == "plane": if container: bc.scale[0] = container.w/2 bc.scale[1] = container.h/2 bc.location[0] = container.x + container.w/2 bc.location[1] = container.y + container.h/2 bpy.ops.object.origin_set(type="ORIGIN_CURSOR") bpy.ops.object.transform_apply() elif _type == "cube": if container: bc.scale[0] = container.w/2 bc.scale[1] = container.h/2 bc.scale[2] = 0.1 bc.location[0] = container.x + container.w/2 bc.location[1] = container.y + container.h/2 bpy.ops.object.origin_set(type="ORIGIN_CURSOR") bpy.ops.object.transform_apply() if material: if material == "auto": mat = bpy.data.materials.new(f"Material_{name}") mat.use_nodes = True bc.data.materials.append(mat) bc = bpy.context.scene.objects[name] bc_coll = BPH.FindCollectionForItem(bc) if bc_coll != coll: coll.objects.link(bc) bc_coll.objects.unlink(bc) bc.select_set(False) bc.data.name = name #print(bc.name, bc.data.name) return bc, created def Vector(pt, z=0): x, y = pt return Vector((x, y, z)) class BlenderPen(BpyObj, DrawablePenMixin, BasePen): def __init__(self, dat): super().__init__(None) self.dat = dat tag = self.dat.tag() self._material = None if tag is None: raise Exception("BlenderPen pens must be tagged") self.tag = tag def record(self, dat): self.set_origin(0, 0, 0) self._spline = None self.splines = [] self._value = [] dat.replay(self) if self._spline and len(self._spline) > 0 and self._spline not in self.splines: self.splines.append(self._spline) x, y = self.dat.ambit().pc #self.set_origin(x/100, y/100, 0) def _moveTo(self, p): if self._spline and len(self._spline) > 0 and self._spline not in self.splines: self.splines.append(self._spline) self._spline = [] self._value.append([p]) self._spline.append(["BEZIER", "start", [p, p, p]]) def _lineTo(self, p): self._value.append([p]) self._spline.append(["BEZIER", "curve", [p, p, p]]) def _curveToOne(self, p1, p2, p3): self._spline[-1][-1][-1] = p1 self._value.append([p1, p2, p3]) self._spline.append(["BEZIER", "curve", [p2, p3, p3]]) def _qCurveToOne(self, p1, p2): start = self._value[-1][-1] q1, q2, q3 = raise_quadratic(start, (p1[0], p1[1]), (p2[0], p2[1])) self._spline[-1][-1][-1] = q1 self._value.append([q1, q2, q3]) self._spline.append(["BEZIER", "curve", [q2, q3, q3]]) def _closePath(self): if self._spline and len(self._spline) > 0 and self._spline not in self.splines: self.splines.append(self._spline) self.spline = None self.spline = None def materials(self): return self.obj.data.materials def bsdf(self): if self._material: try: return self.materials()[0].node_tree.nodes["Principled BSDF"] except: return None def shadow(self, clip=None, radius=10, alpha=0.3, color=Color.from_rgb(0,0,0,1)): pass def setColorValue(self, value, color): value[0] = color.r value[1] = color.g value[2] = color.b value[3] = 1 if color.a != 1: self.transmission(1) #value[0] = 1 #value[1] = 1 #value[2] = 1 #value[3] = 1 def fill(self, color): if not self._material == "auto" or not self.bsdf(): return if color: if isinstance(color, Gradient): self.fill(color.stops[0][0]) else: #print("FILL>>>>>", self.tag, color) bsdf = self.bsdf() dv = bsdf.inputs[0].default_value self.setColorValue(dv, color) def stroke(self, weight=1, color=None, dash=None, miter=None): if not self._material == "auto" or not self.bsdf(): return if weight and color and color.a > 0: #print("STROKE>>>", self.tag, weight, color) self.obj.data.fill_mode = "NONE" if isinstance(color, Gradient): pass else: self.fill(color) def extrude(self, amount=0.1): self.obj.data.extrude = amount return self def bevel(self, depth=0.02): self.obj.data.bevel_depth = depth return self def specular(self, amount=0.5): if not self._material == "auto" or not self.bsdf(): return self.bsdf().inputs[7].default_value = amount return self def metallic(self, amount=1): if not self._material == "auto" or not self.bsdf(): return self.bsdf().inputs[1].default_value = amount return self def roughness(self, amount=0.5): if not self._material == "auto" or not self.bsdf(): return self.bsdf().inputs[2].default_value = amount return self def transmission(self, amount=1): if not self._material == "auto" or not self.bsdf(): return self.bsdf().inputs[18].default_value = amount return self def emission(self, color=None, strength=1): if not self._material == "auto" or not self.bsdf(): return if color is not None: self.setColorValue(self.bsdf().inputs[27].default_value, normalize_color(color)) self.bsdf().inputs[28].default_value = strength return self def image(self, src=None, opacity=1, rect=None, pattern=True): mat = self.materials()[0] bsdf = self.bsdf() if "Image Texture" in mat.node_tree.nodes: imgtex = mat.node_tree.nodes["Image Texture"] else: imgtex = mat.node_tree.nodes.new("ShaderNodeTexImage") mat.node_tree.links.new(bsdf.inputs["Base Color"], imgtex.outputs["Color"]) imgtex.image = bpy.data.images.load(str(src)) return self def at_frame(self, frame, path, value=None): bpy.data.scenes[0].frame_set(frame) if value is not None: if callable(value): value(self) else: exec(f"self.obj.{path} = {value}") self.obj.keyframe_insert(data_path=path) return self #setattr(self.obj, path, value) def hide(self, hide=True): self.obj.hide_viewport = hide self.obj.hide_render = hide return self def set_visibility_at_frame(self, frame, visibility): bpy.data.scenes[0].frame_set(frame) self.hide(not visibility) self.obj.keyframe_insert(data_path="hide_render") self.obj.keyframe_insert(data_path="hide_viewport") return self def show_on_frame(self, frame): self.set_visibility_at_frame(0, False) self.set_visibility_at_frame(frame, True) self.set_visibility_at_frame(frame+1, False) return self def show_at_frame(self, frame): self.set_visibility_at_frame(0, False) self.set_visibility_at_frame(frame-1, False) self.set_visibility_at_frame(frame, True) return self def make_invisible(self): self.obj.cycles_visibility.camera = False self.obj.cycles_visibility.diffuse = False self.obj.cycles_visibility.glossy = False self.obj.cycles_visibility.transmission = False self.obj.cycles_visibility.scatter = False self.obj.cycles_visibility.shadow = False return self def convert_to_mesh(self): bpy.context.view_layer.objects.active = None bpy.context.view_layer.objects.active = self.obj self.obj.select_set(True) bpy.ops.object.convert(target="MESH") self.obj.select_set(False) bpy.context.view_layer.objects.active = None return self convertToMesh = convert_to_mesh def remesh_smooth(self, octree_depth=7, smooth=False): with self.obj_selected() as o: if "Remesh" not in o.modifiers: bpy.ops.object.modifier_add(type="REMESH") rm = o.modifiers["Remesh"] rm.mode = "SMOOTH" rm.octree_depth = octree_depth rm.use_remove_disconnected = False rm.use_smooth_shade = smooth return self def cloth(self, pressure=5): with self.obj_selected() as o: bpy.ops.object.modifier_add(type='CLOTH') cl = o.modifiers["Cloth"] cl.settings.effector_weights.gravity = 0 cl.settings.use_pressure = True cl.settings.uniform_pressure_force = pressure cl.settings.collision_settings.use_self_collision = True return self def apply_transform(self, location=True, rotation=True, scale=True, properties=True ): bpy.context.view_layer.objects.active = None bpy.context.view_layer.objects.active = self.obj self.obj.select_set(True) bpy.ops.object.transform_apply(location=location, rotation=rotation, scale=scale, properties=properties) self.obj.select_set(False) bpy.context.view_layer.objects.active = None return self def with_origin(self, xyz, fn): if xyz == "C": pc = self.dat.ambit().pc xyz = (pc.x/100, pc.y/100, 0) self.set_origin(*xyz) fn(self) self.set_origin(0, 0, 0) return self def center_origin(self): c = self.dat.ambit().pc self.locate_relative(x=-c.x/100, y=-c.y/100) bpy.context.view_layer.objects.active = None bpy.context.view_layer.objects.active = self.obj self.obj.select_set(True) bpy.ops.object.origin_set(type="ORIGIN_CURSOR") self.obj.select_set(False) bpy.context.view_layer.objects.active = None self.locate_relative(x=+c.x/100, y=+c.y/100) return self def draw(self, collection, style=None, scale=0.01, cyclic=True, dn=False, primitive=None, material="auto"): self._material = material if primitive is not None: self.obj, self.created = BPH.Primitive(primitive, collection, self.tag, dn=dn, material=material, container=self.dat.ambit().scale(scale)) else: self.obj, self.created = BPH.Primitive("Bezier", collection, self.tag, dn=dn, material=material, cyclic=cyclic) if cyclic: self.obj.data.fill_mode = "BOTH" self.origin_to_cursor() if cyclic: self.record(self.dat.copy().removeOverlap().scale(scale, point=False)) else: self.record(self.dat.copy().scale(scale, point=False)) try: self.draw_on_bezier_curve(self.obj.data, cyclic=cyclic) except: pass if material and material != "auto": try: mat = bpy.data.materials[material] except KeyError: mat = bpy.data.materials.new(material) mat.use_nodes = True self.obj.data.materials.clear() self.obj.data.materials.append(mat) for attrs, attr in self.findStyledAttrs(style): self.applyDATAttribute(attrs, attr) return self def draw_on_bezier_curve(self, bez, cyclic=True): for spline in reversed(bez.splines): # clear existing splines bez.splines.remove(spline) for spline_data in self.splines: bez.splines.new('BEZIER') spline = bez.splines[-1] spline.use_cyclic_u = cyclic for i, (t, style, pts) in enumerate(spline_data): l, c, r = pts if i > 0: spline.bezier_points.add(1) pt = spline.bezier_points[-1] pt.co = BPH.Vector(c) pt.handle_left = BPH.Vector(l) pt.handle_right = BPH.Vector(r) def noop(self, *args, **kwargs): return self class BlenderPenCube(BlenderPen): def extrude(self, amount=0.1, apply=False): with self.obj_selected(): self.obj.scale[2] = amount*10 if apply: bpy.ops.object.transform_apply() return self ================================================ FILE: packages/coldtype-core/src/coldtype/pens/drawablepen.py ================================================ # Mixin for attribute-application from coldtype.runon import Runon from coldtype.color import Gradient, Color class DrawablePenMixin(object): def print(self, *args): for a in args: if callable(a): print(a(self)) else: print(a) return self def fill(self, el, color): raise Exception("Pen does not implement fill function") def stroke(self, el, weight=1, color=None, dash=None, miter=None): raise Exception("Pen does not implement stroke function") def shadow(self, el, clip=None, radius=10, alpha=0.3, color=Color.from_rgb(0,0,0,1)): raise Exception("Pen does not implement shadow function") def image(self, el, src=None, opacity=None, rect=None, pattern=True): raise Exception("Pen does not implement image function") def applyDATAttribute(self, attrs, attribute): k, v = attribute if v: if k == "shadow": return self.shadow(**v) elif k == "fill": return self.fill(v) elif k == "stroke": return self.stroke(**v, dash=attrs.get("dash")) elif k == "image": return self.image(**v) def findStyledAttrs(self, style): attrs = self.dat.style(style) for attr in attrs.items(): if attr and attr[-1]: yield attrs, attr def FindPens(pens): if not isinstance(pens, Runon): pens = pens[0] found = [] def walker(pen, pos, data): if pos == 0: found.append(pen) pens.walk(walker) for pen in found: yield pen ================================================ FILE: packages/coldtype-core/src/coldtype/pens/drawbotpen.py ================================================ try: import drawBot as db except: pass from coldtype.runon.path import P from coldtype.geometry import Rect, Edge, Point from coldtype.pens.drawablepen import DrawablePenMixin from coldtype.color import Color, Gradient def get_image_rect(src): w, h = db.imageSize(str(src)) return Rect(0, 0, w, h) class DrawBotPen(DrawablePenMixin, P): def __init__(self, dat, rect=None): super().__init__() self.rect = rect self.dat = dat self.bp = db.BezierPath() self.dat.replay(self.bp) def fill(self, color): if color: if isinstance(color, Gradient): self.gradient(color) elif isinstance(color, Color): db.fill(color.r, color.g, color.b, color.a) else: db.fill(None) def stroke(self, weight=1, color=None, dash=None, miter=None): db.strokeWidth(weight) if dash: db.lineDash(dash) if color: if isinstance(color, Gradient): pass # possible? elif isinstance(color, Color): db.stroke(color.r, color.g, color.b, color.a) else: db.stroke(None) def image(self, src=None, opacity=1, rect=None, rotate=0, repeating=False, scale=True): bounds = self.dat.bounds() src = str(src) if not rect: rect = bounds try: img_w, img_h = db.imageSize(src) except ValueError: print("DrawBotPen: No image") return x = bounds.x y = bounds.y if repeating: x_count = bounds.w / rect.w y_count = bounds.h / rect.h else: x_count = 1 y_count = 1 _x = 0 _y = 0 while x <= (bounds.w+bounds.x) and _x < x_count: _x += 1 while y <= (bounds.h+bounds.y) and _y < y_count: _y += 1 with db.savedState(): r = Rect(x, y, rect.w, rect.h) #db.fill(1, 0, 0.5, 0.05) #db.oval(*r) if scale == True: db.scale(rect.w/img_w, center=r.point("SW")) elif scale: try: db.scale(scale[0], scale[1], center=r.point("SW")) except TypeError: db.scale(scale, center=r.point("SW")) db.rotate(rotate) db.image(src, (r.x, r.y), alpha=opacity) y += rect.h y = 0 x += rect.w def shadow(self, clip=None, radius=10, alpha=0.3, color=Color.from_rgb(0,0,0,1)): if clip: cp = P(clip).f(None) bp = db.BezierPath() cp.replay(bp) db.clipPath(bp) #elif self.rect: # cp = P(fill=None).rect(self.rect).xor(self.dat) # bp = db.BezierPath() # cp.replay(bp) # db.clipPath(bp) db.shadow((0, 0), radius*3, list(color.with_alpha(alpha))) def gradient(self, gradient): stops = gradient.stops db.linearGradient(stops[0][1], stops[1][1], [list(s[0]) for s in stops], [0, 1]) def draw(self, scale=1, style=None, attrs=True): if len(self.dat) > 0: for p in self.dat._els: DrawBotPen(p, rect=self.rect).draw(scale=scale, attrs=attrs) else: with db.savedState(): db.scale(scale) if attrs: for attrs, attr in self.findStyledAttrs(style): self.applyDATAttribute(attrs, attr) db.drawPath(self.bp) return self def draw_with_filters(self, rect, filters): im = db.ImageObject() with im: db.size(*rect.wh()) self.draw() for filter_name, filter_kwargs in filters: getattr(im, filter_name)(**filter_kwargs) x, y = im.offset() db.image(im, (x, y)) return self ================================================ FILE: packages/coldtype-core/src/coldtype/pens/jsonpen.py ================================================ import os from fontTools.pens.transformPen import TransformPen from fontTools.pens.basePen import BasePen from coldtype.pens.drawablepen import DrawablePenMixin from coldtype.color import Gradient from coldtype.color import Color import base64 def path_str(*ps): return " ".join([str(int(p)) if float(p).is_integer() else "{:.02f}".format(p) for p in ps]) class JSONPen(DrawablePenMixin, BasePen): def __init__(self, dat, rect): BasePen.__init__(self, None) self.pStr = f"" self.lastMove = None self.dat = dat self.serialAttrs = {"tag":dat.tag()} self.rect = rect if self.rect: tp = TransformPen(self, (1, 0, 0, -1, 0, self.rect.h)) dat.replay(tp) else: dat.replay(self) def _moveTo(self, p): self.lastMove = None self.pStr += "m {:s} ".format(path_str(*p)) def _lineTo(self, p): self.pStr += "{:s}{:s} ".format("" if self.lastMove == "l " else "l ", path_str(*p)) self.lastMove = "l " def _curveToOne(self, p1, p2, p3): self.pStr += "{:s}{:s} ".format("" if self.lastMove == "c " else "c ", path_str(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1])) self.lastMove = "c " def _qCurveToOne(self, p1, p2): self.pStr += "{:s}{:s} ".format("" if self.lastMove == "q " else "q ", path_str(p1[0], p1[1], p2[0], p2[1])) self.lastMove = "q " def _closePath(self): self.pStr += "z " self.lastMove = None def fill(self, color=None): if not color or color == "transparent": self.serialAttrs["fill"] = None elif isinstance(color, Gradient): self.serialAttrs["fill"] = ["g", self.gradient(color)] elif isinstance(color, Color): self.serialAttrs["fill"] = ["f", self.color(color)] def stroke(self, weight=1, color=None, dash=None, miter=None): if color and weight > 0: if color == "transparent": self.serialAttrs["stroke"] = None elif isinstance(color, Color): self.serialAttrs["stroke"] = [self.color(color), weight] def rectPoints(self, rect): return [rect.x, rect.y, rect.w, rect.h] def color(self, color): return [round(c, 2) for c in color.rgba()] def gradient(self, gradient): _gradient = [] for color, position in gradient.stops: _gradient.extend([[self.color(color), [position.x, self.rect.h-position.y]]]) return _gradient def shadow(self, clip=None, radius=14, color=Color.from_rgb(0,0,0,0.3)): if clip: self.serialAttrs["shadow"] = [radius, color.a, self.rectPoints(clip.flip(self.rect.h)), self.color(color.with_alpha(1))] else: self.serialAttrs["shadow"] = [radius, color.a, None] def image(self, src=None, opacity=1, rect=None): self.serialAttrs["pattern"] = [opacity-0.025, base64.b64encode(open(src, "rb").read()).decode('utf-8')] def asCode(self, bounds, style=None): #attrs = self.dat.attrs["dark"] if "dark" in self.dat.attrs else self.dat.attrs["default"] allSerialAttrs = dict() for style, attrs in self.dat.attrs.items(): self.serialAttrs = dict() for attr in attrs.items(): self.applyDATAttribute(attrs, attr) allSerialAttrs[style] = self.serialAttrs allSerialAttrs["d"] = self.pStr if bounds: allSerialAttrs["bounds"] = self.rectPoints(bounds) allSerialAttrs["tag"] = self.dat.tag() return allSerialAttrs def Composite(pens, rect): out = [] for pen in JSONPen.FindPens(pens): jp = JSONPen(pen, rect) out.append(jp.asCode(rect)) return out ================================================ FILE: packages/coldtype-core/src/coldtype/pens/misc.py ================================================ from enum import Enum from fontTools.pens.filterPen import ContourFilterPen from fontTools.pens.recordingPen import RecordingPen from fontTools.misc.bezierTools import splitCubicAtT, calcCubicArcLength USE_SKIA_PATHOPS = True NO_PATHOPS = False try: from pathops import Path, OpBuilder, PathOp, PathVerb except ImportError: USE_SKIA_PATHOPS = False try: from booleanOperations.booleanGlyph import BooleanGlyph from booleanOperations.exceptions import UnsupportedContourError except ImportError: if not USE_SKIA_PATHOPS: USE_SKIA_PATHOPS = True NO_PATHOPS = True class BooleanOp(Enum): Difference = 0 Union = 1 XOR = 2 ReverseDifference = 3 Intersection = 4 Simplify = 5 def Skia(x): return [ PathOp.DIFFERENCE, PathOp.UNION, PathOp.XOR, PathOp.REVERSE_DIFFERENCE, PathOp.INTERSECTION, ][x.value] def BooleanGlyphMethod(x): return [ "difference", "union", "xor", "reverseDifference", "intersection", ][x.value] def calculate_pathop(pen1, pen2, operation, use_skia_pathops_draw=True): if NO_PATHOPS: raise Exception("No pathops library found; please install either skia-pathops or booleanOperations") if USE_SKIA_PATHOPS: p1 = Path() pen1.replay(p1.getPen()) if operation == BooleanOp.Simplify: # ignore pen2 p1.simplify(fix_winding=True, keep_starting_points=True) d0 = RecordingPen() if not use_skia_pathops_draw: for method, pts in p1: if method == PathVerb.MOVE: d0.moveTo(*pts) elif method == PathVerb.CUBIC: d0.curveTo(*pts) elif method == PathVerb.QUAD: d0.qCurveTo(*pts) elif method == PathVerb.LINE: d0.lineTo(*pts) elif method == PathVerb.CLOSE: d0.closePath() else: p1.draw(d0) return d0.value if pen2: p2 = Path() pen2.replay(p2.getPen()) builder = OpBuilder(fix_winding=True, keep_starting_points=True) builder.add(p1, PathOp.UNION) if pen2: builder.add(p2, BooleanOp.Skia(operation)) result = builder.resolve() d0 = RecordingPen() result.draw(d0) return d0.value else: bg = BooleanGlyph() pen1.replay(bg.getPen()) if operation == BooleanOp.Simplify: # ignore pen2 try: bg = bg.removeOverlap() except UnsupportedContourError: print("booleanOperations could not removeOverlap (qcurve present)") pass dp = RecordingPen() bg.draw(dp) return dp.value bg2 = BooleanGlyph() if pen2: pen2.replay(bg2.getPen()) bg = bg._booleanMath(BooleanOp.BooleanGlyphMethod(operation), bg2) dp = RecordingPen() bg.draw(dp) return dp.value class ExplodingPen(ContourFilterPen): def __init__(self, outPen): self._pens = [] super().__init__(outPen) def filterContour(self, contour): self._pens.append(contour) return contour class SmoothPointsPen(ContourFilterPen): def __init__(self, outPen, length=80): super().__init__(outPen) self.length = length def filterContour(self, contour): nc = [] def split_line(pts): p0, p1 = pts nc.append(["lineTo", [p1]]) def split_curve(pts): p0, p1, p2, p3 = pts length_arc = calcCubicArcLength(p0, p1, p2, p3) if length_arc <= self.length: nc.append(["curveTo", pts[1:]]) else: d = self.length / length_arc b = (p0, p1, p2, p3) a, b = splitCubicAtT(*b, d) nc.append(["curveTo", a[1:]]) split_curve(b) for i, (t, pts) in enumerate(contour): if t == "lineTo": p0 = contour[i-1][-1][-1] split_line((p0, pts[0])) elif t == "curveTo": p1, p2, p3 = pts p0 = contour[i-1][-1][-1] split_curve((p0, p1, p2, p3)) else: nc.append([t, pts]) return nc ================================================ FILE: packages/coldtype-core/src/coldtype/pens/outlinepen.py ================================================ """ The MIT License (MIT) Copyright (c) 2016 Frederik Berlaen 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. """ """ Taken from https://github.com/typemytype/outlinerRoboFontExtension """ from fontTools.pens.basePen import BasePen from fontTools.misc.bezierTools import splitCubicAtT from fontTools.pens.recordingPen import RecordingPen, replayRecording from fontTools.pens.pointPen import AbstractPointPen from fontTools.pens.pointPen import ReverseContourPointPen from fontTools.pens.pointPen import PointToSegmentPen from defcon import Glyph from math import sqrt, cos, sin, acos, asin, degrees, radians, pi def roundFloat(f): error = 1000000. return round(f*error)/error def checkSmooth(firstAngle, lastAngle): if firstAngle is None or lastAngle is None: return True error = 4 firstAngle = degrees(firstAngle) lastAngle = degrees(lastAngle) if int(firstAngle) + error >= int(lastAngle) >= int(firstAngle) - error: return True return False def checkInnerOuter(firstAngle, lastAngle): if firstAngle is None or lastAngle is None: return True dirAngle = degrees(firstAngle) - degrees(lastAngle) if dirAngle > 180: dirAngle = 180 - dirAngle elif dirAngle < -180: dirAngle = -180 - dirAngle if dirAngle > 0: return True if dirAngle <= 0: return False def interSect(seg1, seg2): seg1s, seg1e = seg1 seg2s, seg2e = seg2 denom = (seg2e.y - seg2s.y)*(seg1e.x - seg1s.x) - (seg2e.x - seg2s.x)*(seg1e.y - seg1s.y) if roundFloat(denom) == 0: # print('parallel: %s' % denom) return None uanum = (seg2e.x - seg2s.x)*(seg1s.y - seg2s.y) - (seg2e.y - seg2s.y)*(seg1s.x - seg2s.x) ubnum = (seg1e.x - seg1s.x)*(seg1s.y - seg2s.y) - (seg1e.y - seg1s.y)*(seg1s.x - seg2s.x) ua = uanum / denom # ub = ubnum / denom x = seg1s.x + ua*(seg1e.x - seg1s.x) y = seg1s.y + ua*(seg1e.y - seg1s.y) return MathPoint(x, y) def pointOnACurve(p1, c1, c2, p2, value): x1, y1 = p1 cx1, cy1 = c1 cx2, cy2 = c2 x2, y2 = p2 dx = x1 cx = (cx1 - dx) * 3.0 bx = (cx2 - cx1) * 3.0 - cx ax = x2 - dx - cx - bx dy = y1 cy = (cy1 - dy) * 3.0 by = (cy2 - cy1) * 3.0 - cy ay = y2 - dy - cy - by mx = ax*(value)**3 + bx*(value)**2 + cx*(value) + dx my = ay*(value)**3 + by*(value)**2 + cy*(value) + dy return MathPoint(mx, my) class MathPoint(object): def __init__(self, x, y=None): if y is None: x, y = x self.x = x self.y = y def __repr__(self): return "" % (self.x, self.y) def __getitem__(self, index): if index == 0: return self.x if index == 1: return self.y raise IndexError def __iter__(self): for value in [self.x, self.y]: yield value def __add__(self, p): # p + p if not isinstance(p, self.__class__): return self.__class__(self.x + p, self.y + p) return self.__class__(self.x + p.x, self.y + p.y) def __sub__(self, p): # p - p if not isinstance(p, self.__class__): return self.__class__(self.x - p, self.y - p) return self.__class__(self.x - p.x, self.y - p.y) def __mul__(self, p): # p * p if not isinstance(p, self.__class__): return self.__class__(self.x * p, self.y * p) return self.__class__(self.x * p.x, self.y * p.y) def __div__(self, p): # p / p if not isinstance(p, self.__class__): return self.__class__(self.x / p, self.y / p) return self.__class__(self.x / p.x, self.y / p.y) __truediv__ = __div__ def __eq__(self, p): # if p == p if not isinstance(p, self.__class__): return False return roundFloat(self.x) == roundFloat(p.x) and roundFloat(self.y) == roundFloat(p.y) def __ne__(self, p): # if p != p return not self.__eq__(p) def copy(self): return self.__class__(self.x, self.y) def round(self): self.x = round(self.x) self.y = round(self.y) def distance(self, p): return sqrt((p.x - self.x)**2 + (p.y - self.y)**2) def angle(self, other, add=90): # returns the angle of a Line in radians b = other.x - self.x a = other.y - self.y c = sqrt(a**2 + b**2) if c == 0: return None if add is None: return b/c cosAngle = degrees(acos(b/c)) sinAngle = degrees(asin(a/c)) if sinAngle < 0: cosAngle = 360 - cosAngle return radians(cosAngle + add) class CleanPointPen(AbstractPointPen): def __init__(self, pointPen): self.pointPen = pointPen self.currentContour = None def processContour(self): pointPen = self.pointPen contour = self.currentContour index = 0 prevAngle = None toRemove = [] for data in contour: if data["segmentType"] in ["line", "move"]: prevPoint = contour[index-1] if prevPoint["segmentType"] in ["line", "move"]: angle = MathPoint(data["point"]).angle(MathPoint(prevPoint["point"])) if prevAngle is not None and angle is not None and roundFloat(prevAngle) == roundFloat(angle): prevPoint["uniqueID"] = id(prevPoint) toRemove.append(prevPoint) prevAngle = angle else: prevAngle = None else: prevAngle = None index += 1 for data in toRemove: contour.remove(data) pointPen.beginPath() for data in contour: pointPen.addPoint(data["point"], **data) pointPen.endPath() def beginPath(self, identifier=None): assert self.currentContour is None self.currentContour = [] self.onCurve = [] def endPath(self): assert self.currentContour is not None self.processContour() self.currentContour = None def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): data = dict(point=pt, segmentType=segmentType, smooth=smooth, name=name) data.update(kwargs) self.currentContour.append(data) def addComponent(self, glyphName, transform): assert self.currentContour is None self.pointPen.addComponent(glyphName, transform) class OutlinePen(BasePen): pointClass = MathPoint magicCurve = 0.5522847498 def __init__(self, glyphSet, offset=10, contrast=0, contrastAngle=0, connection="square", cap="round", miterLimit=None, closeOpenPaths=True, optimizeCurve=False, preserveComponents=False, filterDoubles=True): BasePen.__init__(self, glyphSet) self.offset = abs(offset) self.contrast = abs(contrast) self.contrastAngle = contrastAngle self._inputmiterLimit = miterLimit if miterLimit is None: miterLimit = self.offset * 2 self.miterLimit = abs(miterLimit) self.closeOpenPaths = closeOpenPaths self.optimizeCurve = optimizeCurve self.connectionCallback = getattr(self, "connection%s" % (connection.title())) self.capCallback = getattr(self, "cap%s" % (cap.title())) self.originalGlyph = Glyph() self.originalPen = self.originalGlyph.getPen() self.outerGlyph = Glyph() self.outerPen = self.outerGlyph.getPen() self.outerCurrentPoint = None self.outerFirstPoint = None self.outerPrevPoint = None self.innerGlyph = Glyph() self.innerPen = self.innerGlyph.getPen() self.innerCurrentPoint = None self.innerFirstPoint = None self.innerPrevPoint = None self.prevPoint = None self.firstPoint = None self.firstAngle = None self.prevAngle = None self.shouldHandleMove = True self.preserveComponents = preserveComponents self.components = [] self.filterDoubles = filterDoubles self.drawSettings() def _moveTo(self, pt): x, y = pt if self.offset == 0: self.outerPen.moveTo((x, y)) self.innerPen.moveTo((x, y)) return self.originalPen.moveTo((x, y)) p = self.pointClass(x, y) self.prevPoint = p self.firstPoint = p self.shouldHandleMove = True def _lineTo(self, pt): x, y = pt if self.offset == 0: self.outerPen.lineTo((x, y)) self.innerPen.lineTo((x, y)) return self.originalPen.lineTo((x, y)) currentPoint = self.pointClass(x, y) if currentPoint == self.prevPoint: return self.currentAngle = self.prevPoint.angle(currentPoint) thickness = self.getThickness(self.currentAngle) self.innerCurrentPoint = self.prevPoint - self.pointClass(cos(self.currentAngle), sin(self.currentAngle)) * thickness self.outerCurrentPoint = self.prevPoint + self.pointClass(cos(self.currentAngle), sin(self.currentAngle)) * thickness if self.shouldHandleMove: self.shouldHandleMove = False self.innerPen.moveTo(self.innerCurrentPoint) self.innerFirstPoint = self.innerCurrentPoint self.outerPen.moveTo(self.outerCurrentPoint) self.outerFirstPoint = self.outerCurrentPoint self.firstAngle = self.currentAngle else: self.buildConnection() self.innerCurrentPoint = currentPoint - self.pointClass(cos(self.currentAngle), sin(self.currentAngle)) * thickness self.innerPen.lineTo(self.innerCurrentPoint) self.innerPrevPoint = self.innerCurrentPoint self.outerCurrentPoint = currentPoint + self.pointClass(cos(self.currentAngle), sin(self.currentAngle)) * thickness self.outerPen.lineTo(self.outerCurrentPoint) self.outerPrevPoint = self.outerCurrentPoint self.prevPoint = currentPoint self.prevAngle = self.currentAngle def _curveToOne(self, pt1, pt2, pt3): if self.optimizeCurve: curves = splitCubicAtT(self.prevPoint, pt1, pt2, pt3, .5) else: curves = [(self.prevPoint, pt1, pt2, pt3)] for curve in curves: p1, h1, h2, p2 = curve self._processCurveToOne(h1, h2, p2) def _processCurveToOne(self, pt1, pt2, pt3): if self.offset == 0: self.outerPen.curveTo(pt1, pt2, pt3) self.innerPen.curveTo(pt1, pt2, pt3) return self.originalPen.curveTo(pt1, pt2, pt3) p1 = self.pointClass(*pt1) p2 = self.pointClass(*pt2) p3 = self.pointClass(*pt3) if p1 == self.prevPoint: p1 = pointOnACurve(self.prevPoint, p1, p2, p3, 0.01) if p2 == p3: p2 = pointOnACurve(self.prevPoint, p1, p2, p3, 0.99) a1 = self.prevPoint.angle(p1) a2 = p2.angle(p3) self.currentAngle = a1 tickness1 = self.getThickness(a1) tickness2 = self.getThickness(a2) a1bis = self.prevPoint.angle(p1, 0) a2bis = p3.angle(p2, 0) intersectPoint = interSect((self.prevPoint, self.prevPoint + self.pointClass(cos(a1), sin(a1)) * 100), (p3, p3 + self.pointClass(cos(a2), sin(a2)) * 100)) self.innerCurrentPoint = self.prevPoint - self.pointClass(cos(a1), sin(a1)) * tickness1 self.outerCurrentPoint = self.prevPoint + self.pointClass(cos(a1), sin(a1)) * tickness1 if self.shouldHandleMove: self.shouldHandleMove = False self.innerPen.moveTo(self.innerCurrentPoint) self.innerFirstPoint = self.innerPrevPoint = self.innerCurrentPoint self.outerPen.moveTo(self.outerCurrentPoint) self.outerFirstPoint = self.outerPrevPoint = self.outerCurrentPoint self.firstAngle = a1 else: self.buildConnection() h1 = None if intersectPoint is not None: h1 = interSect((self.innerCurrentPoint, self.innerCurrentPoint + self.pointClass(cos(a1bis), sin(a1bis)) * tickness1), (intersectPoint, p1)) if h1 is None: h1 = p1 - self.pointClass(cos(a1), sin(a1)) * tickness1 self.innerCurrentPoint = p3 - self.pointClass(cos(a2), sin(a2)) * tickness2 h2 = None if intersectPoint is not None: h2 = interSect((self.innerCurrentPoint, self.innerCurrentPoint + self.pointClass(cos(a2bis), sin(a2bis)) * tickness2), (intersectPoint, p2)) if h2 is None: h2 = p2 - self.pointClass(cos(a1), sin(a1)) * tickness1 self.innerPen.curveTo(h1, h2, self.innerCurrentPoint) self.innerPrevPoint = self.innerCurrentPoint ######## h1 = None if intersectPoint is not None: h1 = interSect((self.outerCurrentPoint, self.outerCurrentPoint + self.pointClass(cos(a1bis), sin(a1bis)) * tickness1), (intersectPoint, p1)) if h1 is None: h1 = p1 + self.pointClass(cos(a1), sin(a1)) * tickness1 self.outerCurrentPoint = p3 + self.pointClass(cos(a2), sin(a2)) * tickness2 h2 = None if intersectPoint is not None: h2 = interSect((self.outerCurrentPoint, self.outerCurrentPoint + self.pointClass(cos(a2bis), sin(a2bis)) * tickness2), (intersectPoint, p2)) if h2 is None: h2 = p2 + self.pointClass(cos(a1), sin(a1)) * tickness1 self.outerPen.curveTo(h1, h2, self.outerCurrentPoint) self.outerPrevPoint = self.outerCurrentPoint self.prevPoint = p3 self.currentAngle = a2 self.prevAngle = a2 def _closePath(self): if self.shouldHandleMove: return if self.offset == 0: self.outerPen.closePath() self.innerPen.closePath() return if not self.prevPoint == self.firstPoint: self._lineTo(self.firstPoint) self.originalPen.closePath() self.innerPrevPoint = self.innerCurrentPoint self.innerCurrentPoint = self.innerFirstPoint self.outerPrevPoint = self.outerCurrentPoint self.outerCurrentPoint = self.outerFirstPoint self.prevAngle = self.currentAngle self.currentAngle = self.firstAngle self.buildConnection(close=True) self.innerPen.closePath() self.outerPen.closePath() def _endPath(self): if self.shouldHandleMove: return self.originalPen.endPath() self.innerPen.endPath() self.outerPen.endPath() if self.closeOpenPaths: innerContour = self.innerGlyph[-1] outerContour = self.outerGlyph[-1] innerContour.reverse() innerContour[0].segmentType = "line" outerContour[0].segmentType = "line" self.buildCap(outerContour, innerContour) for point in innerContour: outerContour.addPoint((point.x, point.y), segmentType=point.segmentType, smooth=point.smooth) self.innerGlyph.removeContour(innerContour) def addComponent(self, glyphName, transform): if self.preserveComponents: self.components.append((glyphName, transform)) else: BasePen.addComponent(self, glyphName, transform) # thickness def getThickness(self, angle): a2 = angle + pi * .5 f = abs(sin(a2 + radians(self.contrastAngle))) f = f ** 5 return self.offset + self.contrast * f # connections def buildConnection(self, close=False): if not checkSmooth(self.prevAngle, self.currentAngle): if checkInnerOuter(self.prevAngle, self.currentAngle): self.connectionCallback(self.outerPrevPoint, self.outerCurrentPoint, self.outerPen, close) self.connectionInnerCorner(self.innerPrevPoint, self.innerCurrentPoint, self.innerPen, close) else: self.connectionCallback(self.innerPrevPoint, self.innerCurrentPoint, self.innerPen, close) self.connectionInnerCorner(self.outerPrevPoint, self.outerCurrentPoint, self.outerPen, close) elif not self.filterDoubles: self.innerPen.lineTo(self.innerCurrentPoint) self.outerPen.lineTo(self.outerCurrentPoint) def connectionSquare(self, first, last, pen, close): angle_1 = radians(degrees(self.prevAngle)+90) angle_2 = radians(degrees(self.currentAngle)+90) tempFirst = first - self.pointClass(cos(angle_1), sin(angle_1)) * self.miterLimit tempLast = last + self.pointClass(cos(angle_2), sin(angle_2)) * self.miterLimit newPoint = interSect((first, tempFirst), (last, tempLast)) if newPoint is not None: if self._inputmiterLimit is not None and roundFloat(newPoint.distance(first)) > self._inputmiterLimit: pen.lineTo(tempFirst) pen.lineTo(tempLast) else: pen.lineTo(newPoint) if not close: pen.lineTo(last) def connectionRound(self, first, last, pen, close): angle_1 = radians(degrees(self.prevAngle)+90) angle_2 = radians(degrees(self.currentAngle)+90) tempFirst = first - self.pointClass(sin(angle_1), -cos(angle_1)) tempLast = last + self.pointClass(sin(angle_2), -cos(angle_2)) centerPoint = interSect((first, tempFirst), (last, tempLast)) if centerPoint is None: # the lines are parallel, let's just take the middle centerPoint = (first + last) / 2 angle_diff = (angle_1 - angle_2) % (2 * pi) if angle_diff > pi: angle_diff = 2 * pi - angle_diff angle_half = angle_diff / 2 radius = centerPoint.distance(first) D = radius * (1 - cos(angle_half)) if sin(angle_half) == 0: handleLength = 0 else: handleLength = (4 * D / 3) / sin(angle_half) # length of the bcp line bcp1 = first - self.pointClass(cos(angle_1), sin(angle_1)) * handleLength bcp2 = last + self.pointClass(cos(angle_2), sin(angle_2)) * handleLength pen.curveTo(bcp1, bcp2, last) def connectionButt(self, first, last, pen, close): if not close: pen.lineTo(last) def connectionInnerCorner(self, first, last, pen, close): if not close: pen.lineTo(last) # caps def buildCap(self, firstContour, lastContour): first = firstContour[-1] last = lastContour[0] first = self.pointClass(first.x, first.y) last = self.pointClass(last.x, last.y) self.capCallback(firstContour, lastContour, first, last, self.prevAngle) first = lastContour[-1] last = firstContour[0] first = self.pointClass(first.x, first.y) last = self.pointClass(last.x, last.y) angle = radians(degrees(self.firstAngle) + 180) self.capCallback(lastContour, firstContour, first, last, angle) def capButt(self, firstContour, lastContour, first, last, angle): # not nothing pass def capRound(self, firstContour, lastContour, first, last, angle): hookedAngle = radians(degrees(angle) + 90) p1 = first - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset p2 = last - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset oncurve = p1 + (p2 - p1) * .5 roundness = .54 # should be self.magicCurve h1 = first - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset * roundness h2 = oncurve + self.pointClass(cos(angle), sin(angle)) * self.offset * roundness firstContour[-1].smooth = True firstContour.addPoint((h1.x, h1.y)) firstContour.addPoint((h2.x, h2.y)) firstContour.addPoint((oncurve.x, oncurve.y), smooth=True, segmentType="curve") h1 = oncurve - self.pointClass(cos(angle), sin(angle)) * self.offset * roundness h2 = last - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset * roundness firstContour.addPoint((h1.x, h1.y)) firstContour.addPoint((h2.x, h2.y)) lastContour[0].segmentType = "curve" lastContour[0].smooth = True def capSquare(self, firstContour, lastContour, first, last, angle): angle = radians(degrees(angle) + 90) firstContour[-1].smooth = True lastContour[0].smooth = True p1 = first - self.pointClass(cos(angle), sin(angle)) * self.offset firstContour.addPoint((p1.x, p1.y), smooth=False, segmentType="line") p2 = last - self.pointClass(cos(angle), sin(angle)) * self.offset firstContour.addPoint((p2.x, p2.y), smooth=False, segmentType="line") def drawSettings(self, drawOriginal=False, drawInner=False, drawOuter=True): self.drawOriginal = drawOriginal self.drawInner = drawInner self.drawOuter = drawOuter def drawPoints(self, pointPen): if self.drawInner: reversePen = ReverseContourPointPen(pointPen) self.innerGlyph.drawPoints(CleanPointPen(reversePen)) if self.drawOuter: self.outerGlyph.drawPoints(CleanPointPen(pointPen)) if self.drawOriginal: if self.drawOuter: pointPen = ReverseContourPointPen(pointPen) self.originalGlyph.drawPoints(CleanPointPen(pointPen)) for glyphName, transform in self.components: pointPen.addComponent(glyphName, transform) def draw(self, pen): pointPen = PointToSegmentPen(pen) self.drawPoints(pointPen) def getGlyph(self): glyph = Glyph() pointPen = glyph.getPointPen() self.drawPoints(pointPen) return glyph def Record(recording, offset=1): op = OutlinePen(None, offset=offset, optimizeCurve=True) replayRecording(recording.value, op) op.drawSettings(drawInner=True, drawOuter=True) g = op.getGlyph() rp2 = RecordingPen() g.draw(rp2) return rp2 ================================================ FILE: packages/coldtype-core/src/coldtype/pens/rendererdrawbotpen.py ================================================ try: import drawBot as db except: pass from coldtype.pens.drawbotpen import DrawBotPen class RendererDrawBotPen(DrawBotPen): #def draw(self, scale=2, style=None): # return super().draw(scale=scale, style=style) def Composite1(pens, rect, save_to, paginate=False, scale=2): db.newDrawing() rect = rect.scale(scale) if not paginate: db.newPage(rect.w, rect.h) for pen in RendererDrawBotPen.FindPens(pens): if paginate: db.newPage(rect.w, rect.h) RendererDrawBotPen(pen, rect).draw(scale=scale) db.saveImage(str(save_to)) db.endDrawing() def Composite(pens, rect, save_to, scale=2): db.newDrawing() rect = rect.scale(scale) db.newPage(rect.w, rect.h) def draw(pen, state, data): if state == 0: RendererDrawBotPen(pen, rect).draw(scale=scale) elif state == -1: imgf = pen.data("imgf") if imgf: im = db.ImageObject() im.lockFocus() db.size(rect.w+300, rect.h+300) db.translate(150, 150) db.scale(scale) pen.data(im=im) elif state == 1: imgf = pen.data("imgf") im = pen.data("im") if imgf and im: im.unlockFocus() imgf(im) x, y = im.offset() db.translate(-150, -150) db.image(im, (x, y)) if not hasattr(pens, "_pens"): pens = [pens] for dps in pens: dps.walk(draw) db.saveImage(str(save_to)) db.endDrawing() ================================================ FILE: packages/coldtype-core/src/coldtype/pens/reportlabpen.py ================================================ from fontTools.pens.basePen import BasePen from reportlab.pdfgen.canvas import Canvas from reportlab.lib.pagesizes import letter from reportlab.lib.colors import transparent from coldtype.geometry import Point from coldtype.pens.drawablepen import DrawablePenMixin class ReportLabPathPen(BasePen): def __init__(self, canvas): super().__init__() self.path = canvas.beginPath() def _moveTo(self, p): (x,y) = p self.path.moveTo(x,y) def _lineTo(self, p): (x,y) = p self.path.lineTo(x,y) def _curveToOne(self, p1, p2, p3): (x1,y1) = p1 (x2,y2) = p2 (x3,y3) = p3 self.path.curveTo(x1, y1, x2, y2, x3, y3) def _closePath(self): self.path.close() class ReportLabPen(DrawablePenMixin): def __init__(self, dat, rect, canvas, scale, style=None, alpha=1): super().__init__() self.dat = dat self.scale = scale self.canvas = canvas self.rect = rect self.style = style self.alpha = alpha rpp = ReportLabPathPen(canvas) dat.replay(rpp) self.path = rpp.path all_attrs = list(self.findStyledAttrs(style)) canvas.saveState() for attrs, attr in all_attrs: method, *args = attr do_stroke = 0 do_fill = 0 if method == "stroke" and args[0].get("weight") != 0: do_stroke = 1 elif method == "fill" and args[0].a > 0: do_fill = 1 self.applyDATAttribute(attrs, attr) canvas.drawPath(self.path, fill=do_fill, stroke=do_stroke) canvas.restoreState() def fill(self, color): self.canvas.setFillColorRGB(color.r, color.g, color.b) def stroke(self, weight=1, color=None, dash=None, miter=None): #return if weight == 0: self.canvas.setStrokeColor(transparent) self.canvas.setLineWidth(0) return self.canvas.setStrokeColorRGB(color.r, color.g, color.b) self.canvas.setLineWidth(weight) def PDF(pens, rect, save_to, scale=1, style=None, title=None): c = Canvas(save_to, pagesize=letter) ReportLabPen.CompositeToCanvas(pens, rect, c, scale=scale, style=style) if title: c.setTitle(title) c.showPage() c.save() # necessary? def CompositeToCanvas(pens, rect, canvas, scale=1, style=None): if scale != 1: pens.scale(scale, scale, Point((0, 0))) if not pens.visible: return def draw(pen, state, data): if state != 0 or not pen.visible: return if "text" in pen._data: # TODO, look at SkiaPen for reference implementation return if state == 0: ReportLabPen(pen, rect, canvas, scale, style=style, alpha=data["alpha"]) pens.walk(draw, visible_only=True) ================================================ FILE: packages/coldtype-core/src/coldtype/pens/skiapathpen.py ================================================ from fontTools.pens.basePen import BasePen from fontTools.pens.transformPen import TransformPen from coldtype.runon.path import P try: import skia except ImportError: skia = None class SkiaPathPen(BasePen): def __init__(self, dat, h=None): super().__init__() self.dat = dat self.path = skia.Path() if h is not None: tp = TransformPen(self, (1, 0, 0, -1, 0, h)) if hasattr(dat, "_val"): try: dat._val.replay(tp) except TypeError: #print("FAIL") #print(dat._val.value) pass else: dat.replay(tp) else: for mv, pts in self.dat.value: #if mv == "qCurveTo": # self._qCurveToOne() getattr(self, mv)(*pts) def _moveTo(self, p): self.path.moveTo(p[0], p[1]) def _lineTo(self, p): self.path.lineTo(p[0], p[1]) def _curveToOne(self, p1, p2, p3): self.path.cubicTo(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]) def _qCurveToOne(self, p1, p2): self.path.quadTo(p1[0], p1[1], p2[0], p2[1]) def _closePath(self): self.path.close() def to_drawing(self): def unwrap(p): return [p.x(), p.y()] dp = P() for mv, pts in self.path: if mv == skia.Path.Verb.kMove_Verb: dp.moveTo(unwrap(pts[0])) elif mv == skia.Path.Verb.kLine_Verb: dp.lineTo(*[unwrap(p) for p in pts[1:]]) elif mv == skia.Path.Verb.kQuad_Verb: dp.qCurveTo(*[unwrap(p) for p in pts[1:]]) elif mv == skia.Path.Verb.kClose_Verb: dp.closePath() else: print(mv) return dp ================================================ FILE: packages/coldtype-core/src/coldtype/pens/skiapen.py ================================================ import skia, struct from pathlib import Path from coldtype.pens.drawablepen import DrawablePenMixin, Gradient from coldtype.pens.skiapathpen import SkiaPathPen from coldtype.runon.path import P from coldtype.img.abstract import AbstractImage from coldtype.geometry import Rect, Point from coldtype.text.reader import Style from coldtype.color import Color import coldtype.skiashim as skiashim from coldtype.img.skiasvg import SkiaSVG try: from coldtype.text.colr.skia import SkiaShaders except ImportError: pass class SkiaPen(DrawablePenMixin, SkiaPathPen): def __init__(self, dat, rect, canvas, scale, style=None, alpha=1): super().__init__(dat, rect.h) self.scale = scale self.canvas = canvas self.rect = rect self.blendmode = None self.style = style self.alpha = alpha all_attrs = list(self.findStyledAttrs(style)) skia_paint_kwargs = dict(AntiAlias=True) for attrs, attr in all_attrs: method, *args = attr if method == "skp": skia_paint_kwargs = args[0] if "AntiAlias" not in skia_paint_kwargs: skia_paint_kwargs["AntiAlias"] = True elif method == "blendmode": self.blendmode = args[0].to_skia() for attrs, attr in all_attrs: filtered_paint_kwargs = {} for k, v in skia_paint_kwargs.items(): if not k.startswith("_"): filtered_paint_kwargs[k] = v #filtered_paint_kwargs["AntiAlias"] = False self.paint = skia.Paint(**filtered_paint_kwargs) if self.blendmode: self.paint.setBlendMode(self.blendmode) method, *args = attr if method == "skp": pass elif method == "skb": pass elif method == "blendmode": pass elif method == "stroke" and args[0].get("weight") == 0: pass elif method == "dash": pass else: canvas.save() if method == "COLR": did_draw = False self.colr(args[0], dat) else: did_draw = self.applyDATAttribute(attrs, attr) self.paint.setAlphaf(self.paint.getAlphaf()*self.alpha) if not did_draw: canvas.drawPath(self.path, self.paint) canvas.restore() def colr(self, data, pen:P): method, args = data shader_fn = getattr(SkiaShaders, method) if shader_fn: ss = pen.data("substructure").copy() ss.invertYAxis(self.rect.h) sval = ss._val.value if method == "drawPathLinearGradient": args["pt1"] = sval[0][1][0] args["pt2"] = sval[1][1][0] elif method == "drawPathSweepGradient": args["center"] = sval[0][1][0] elif method == "drawPathRadialGradient": args["startCenter"] = sval[0][1][0] args["endCenter"] = sval[1][1][0] shader = shader_fn(*args.values()) self.paint.setStyle(skia.Paint.kFill_Style) self.paint.setShader(shader) else: raise Exception("No matching SkiaShaders function for " + method) def fill(self, color): self.paint.setStyle(skia.Paint.kFill_Style) if color: if isinstance(color, Gradient): self.gradient(color) elif isinstance(color, Color): self.paint.setColor(color.skia()) if "blur" in self.dat._data: args = self.dat._data["blur"] try: sigma = args[0] / 3 if len(args) > 1: style = args[1] else: style = skia.kNormal_BlurStyle except: style = skia.kNormal_BlurStyle sigma = args / 3 if sigma > 0: self.paint.setMaskFilter(skia.MaskFilter.MakeBlur(style, sigma)) if "shake" in self.dat._data: args = self.dat._data["shake"] self.paint.setPathEffect(skia.DiscretePathEffect.Make(*args)) def stroke(self, weight=1, color=None, dash=None, miter=None): self.paint.setStyle(skia.Paint.kStroke_Style) if dash: self.paint.setPathEffect(skia.DashPathEffect.Make(*dash)) if color and weight > 0: self.paint.setStrokeWidth(weight*self.scale) if miter: self.paint.setStrokeMiter(miter) if isinstance(color, Gradient): self.gradient(color) else: self.paint.setColor(color.skia()) def gradient(self, gradient): self.paint.setShader(skia.GradientShader.MakeLinear([s[1].flip(self.rect).xy() for s in gradient.stops], [s[0].skia() for s in gradient.stops])) def image(self, src=None, opacity=1, rect=None, pattern=True): if isinstance(src, skia.Image): image = src else: image = skia.Image.MakeFromEncoded(skia.Data.MakeFromFileName(str(src))) if not image: print("image <", src, "> not found, cannot be used") return _, _, iw, ih = image.bounds() if pattern: matrix = skia.Matrix() matrix.setScale(rect.w / iw, rect.h / ih) self.paint.setShader(skiashim.image_makeShader(image, matrix)) if opacity != 1: tf = skia.ColorFilters.Matrix([ 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, opacity, 0 ]) cf = self.paint.getColorFilter() if cf: self.paint.setColorFilter(skia.ColorFilters.Compose( tf, cf)) else: self.paint.setColorFilter(tf) if not pattern: bx, by, bw, bh = self.path.getBounds() if rect: bx, by = rect.flip(self.rect.h).xy() #bx += rx #by += ry sx = rect.w / iw sy = rect.h / ih self.canvas.save() #self.canvas.setMatrix(matrix) self.canvas.clipPath(self.path, doAntiAlias=True) if False: self.canvas.scale(sx, sy) else: # TODO scale the image, or maybe that shouldn't be here? this scaling method is horrible for image quality self.canvas.scale(sx, sy) was_alpha = self.paint.getAlphaf() paint = skiashim.paint_withFilterQualityHigh() paint.setAlphaf(was_alpha*self.alpha) skiashim.canvas_drawImage(self.canvas, image, bx/sx, by/sy, self.paint) self.canvas.restore() return True def shadow(self, clip=None, radius=10, color=Color.from_rgb(0,0,0,1)): #print("SHADOW>", self.style, clip, radius, color) if clip: if isinstance(clip, Rect): skia.Rect() sr = skia.Rect(*clip.scale(self.scale, "mnx", "mny").flip(self.rect.h).mnmnmxmx()) self.canvas.clipRect(sr) elif isinstance(clip, P): sp = SkiaPathPen(clip, self.rect.h) self.canvas.clipPath(sp.path, doAntiAlias=True) self.paint.setColor(skia.ColorBLACK) self.paint.setImageFilter(skia.ImageFilters.DropShadow(0, 0, radius, radius, color.skia())) return def Composite(pens, rect, save_to, scale=1, context=None, style=None): rect = rect.scale(scale).round() if context: info = skia.ImageInfo.MakeN32Premul(rect.w, rect.h) surface = skia.Surface.MakeRenderTarget(context, skia.Budgeted.kNo, info) if not surface: print("SURFACE CREATION FAILED, USING CPU...") surface = skia.Surface(rect.w, rect.h) else: #print("CPU RENDER") surface = skia.Surface(rect.w, rect.h) with surface as canvas: if callable(pens): pens(canvas) # direct-draw else: SkiaPen.CompositeToCanvas(pens, rect, canvas, scale=scale, style=style) image = surface.makeImageSnapshot() image.save(save_to, skia.kPNG) def PDFOnePage(pens, rect, save_to, scale=1): stream = skia.FILEWStream(str(save_to)) with skia.PDF.MakeDocument(stream) as document: with document.page(rect.w, rect.h) as canvas: SkiaPen.CompositeToCanvas(pens, rect, canvas, scale=scale) def PDFMultiPage(pages, rect, save_to, scale=1): stream = skia.FILEWStream(str(save_to)) with skia.PDF.MakeDocument(stream) as document: for page in pages: with document.page(rect.w, rect.h) as canvas: SkiaPen.CompositeToCanvas(page, rect, canvas, scale=scale) def SVG(pens, rect, save_to, scale=1): stream = skia.FILEWStream(str(save_to)) canvas = skia.SVGCanvas.Make((rect.w, rect.h), stream) SkiaPen.CompositeToCanvas(pens, rect, canvas, scale=scale) del canvas stream.flush() def CompositeToCanvas(pens, rect, canvas, scale=1, style=None): #import inspect #curframe = inspect.currentframe() #calframe = inspect.getouterframes(curframe, 2) #print(calframe[1][3],"-> CompositeToCanvas:", pens) style_ = style if scale != 1: pens.scale(scale, scale, Point((0, 0))) if not pens.visible: return def draw(pen, state, data): if state != 0: return if not pen.visible: return if "text" in pen._data: text = pen.data("text") style = pen.data("style") frame = pen.ambit() if not isinstance(style, Style): style = Style(*style[:-1], **style[-1], load_font=0) if isinstance(style.font, str): font = skia.Typeface(style.font) else: font = skia.Typeface.MakeFromFile(str(style.font.path)) if len(style.variations) > 0: fa = skia.FontArguments() # h/t https://github.com/justvanrossum/drawbot-skia/blob/master/src/drawbot_skia/gstate.py to_int = lambda s: struct.unpack(">i", bytes(s, "ascii"))[0] makeCoord = skia.FontArguments.VariationPosition.Coordinate rawCoords = [makeCoord(to_int(tag), value) for tag, value in style.variations.items()] coords = skia.FontArguments.VariationPosition.Coordinates(rawCoords) fa.setVariationDesignPosition(skia.FontArguments.VariationPosition(coords)) font = font.makeClone(fa) pt = frame.point("SW") canvas.drawString( text, pt.x, rect.h - pt.y, skia.Font(font, style.fontSize), skia.Paint(AntiAlias=True, Color=style.fill.skia())) return elif isinstance(pen, SkiaSVG): #print("HELLO?", pen._img, pen.width(), pen.height()) pen._img.render(canvas) elif isinstance(pen, AbstractImage): paint = skiashim.paint_withFilterQualityHigh() f = pen.data("frame") canvas.save() for action, *args in pen.transforms: if action == "rotate": deg, pt = args canvas.rotate(-deg, pt.x, rect.h - pt.y) elif action == "matrix": xs = args a, b, c, d, e, f = xs[0] m = skia.Matrix([ a, b, e, c, d, f, 0, 0, 1 ]) canvas.setMatrix(m) paint.setAlphaf(paint.getAlphaf()*data["alpha"]*pen.alpha) bm = pen.blendmode() if bm: paint.setBlendMode(bm.to_skia()) skiashim.canvas_drawImage(canvas, pen._img, f.x, rect.h - f.y - f.h, paint) canvas.restore() return if state == 0: SkiaPen(pen, rect, canvas, scale, style=style_, alpha=data["alpha"]) pens.walk(draw, visible_only=True) def Precompose(pens, rect, fmt=None, context=None, scale=1, disk=False, style=None): rect = rect.round() if scale < 0: rescale = abs(scale) scale = 1 else: rescale = None sr = rect if scale != 1: sr = rect.scale(scale).round() rect = rect.round() if context: info = skia.ImageInfo.MakeN32Premul(sr.w, sr.h) surface = skia.Surface.MakeRenderTarget(context, skia.Budgeted.kNo, info) assert surface is not None else: surface = skia.Surface(sr.w, sr.h) with surface as canvas: canvas.save() canvas.scale(scale, scale) if callable(pens): pens(canvas) else: SkiaPen.CompositeToCanvas(pens.translate(-rect.x, -rect.y), rect, canvas, style=style) canvas.restore() img = surface.makeImageSnapshot() if rescale is not None: x, y, w, h = rect.scale(rescale) img = img.resize(int(w), int(h)) if disk: Path(disk).parent.mkdir(exist_ok=True, parents=True) img.save(disk, skia.kPNG) #return disk return img def ReadImage(src): return skia.Image.MakeFromEncoded(skia.Data.MakeFromFileName(str(src))) ================================================ FILE: packages/coldtype-core/src/coldtype/pens/svgpen.py ================================================ from fontTools.pens.transformPen import TransformPen from fontTools.pens.basePen import BasePen from coldtype.color import Gradient, Color from coldtype.pens.drawablepen import DrawablePenMixin #from lxml import etree import xml.etree.ElementTree as etree import base64 from random import randint def pointToString(pt): return " ".join(["{:0.1f}".format(i) for i in pt]) class SVGPathPen(BasePen): def __init__(self, glyphSet): BasePen.__init__(self, glyphSet) self._commands = [] self._lastCommand = None self._lastX = None self._lastY = None def _handleAnchor(self): """ >>> pen = SVGPathPen(None) >>> pen.moveTo((0, 0)) >>> pen.moveTo((10, 10)) >>> pen._commands ['M10 10'] """ if self._lastCommand == "M": self._commands.pop(-1) def _moveTo(self, pt): """ >>> pen = SVGPathPen(None) >>> pen.moveTo((0, 0)) >>> pen._commands ['M0 0'] >>> pen = SVGPathPen(None) >>> pen.moveTo((10, 0)) >>> pen._commands ['M10 0'] >>> pen = SVGPathPen(None) >>> pen.moveTo((0, 10)) >>> pen._commands ['M0 10'] """ self._handleAnchor() t = "M%s" % (pointToString(pt)) self._commands.append(t) self._lastCommand = "M" self._lastX, self._lastY = pt def _lineTo(self, pt): """ # duplicate point >>> pen = SVGPathPen(None) >>> pen.moveTo((10, 10)) >>> pen.lineTo((10, 10)) >>> pen._commands ['M10 10'] # vertical line >>> pen = SVGPathPen(None) >>> pen.moveTo((10, 10)) >>> pen.lineTo((10, 0)) >>> pen._commands ['M10 10', 'V0'] # horizontal line >>> pen = SVGPathPen(None) >>> pen.moveTo((10, 10)) >>> pen.lineTo((0, 10)) >>> pen._commands ['M10 10', 'H0'] # basic >>> pen = SVGPathPen(None) >>> pen.lineTo((70, 80)) >>> pen._commands ['L70 80'] # basic following a moveto >>> pen = SVGPathPen(None) >>> pen.moveTo((0, 0)) >>> pen.lineTo((10, 10)) >>> pen._commands ['M0 0', ' 10 10'] """ x, y = pt # duplicate point if x == self._lastX and y == self._lastY: return # vertical line elif x == self._lastX: cmd = "V" pts = str(y) # horizontal line elif y == self._lastY: cmd = "H" pts = str(x) # previous was a moveto elif self._lastCommand == "M": cmd = None pts = " " + pointToString(pt) # basic else: cmd = "L" pts = pointToString(pt) # write the string t = "" if cmd: t += cmd self._lastCommand = cmd t += pts self._commands.append(t) # store for future reference self._lastX, self._lastY = pt def _curveToOne(self, pt1, pt2, pt3): """ >>> pen = SVGPathPen(None) >>> pen.curveTo((10, 20), (30, 40), (50, 60)) >>> pen._commands ['C10 20 30 40 50 60'] """ t = "C" t += pointToString(pt1) + " " t += pointToString(pt2) + " " t += pointToString(pt3) self._commands.append(t) self._lastCommand = "C" self._lastX, self._lastY = pt3 def _qCurveToOne(self, pt1, pt2): """ >>> pen = SVGPathPen(None) >>> pen.qCurveTo((10, 20), (30, 40)) >>> pen._commands ['Q10 20 30 40'] """ assert pt2 is not None t = "Q" t += pointToString(pt1) + " " t += pointToString(pt2) self._commands.append(t) self._lastCommand = "Q" self._lastX, self._lastY = pt2 def _closePath(self): """ >>> pen = SVGPathPen(None) >>> pen.closePath() >>> pen._commands ['Z'] """ self._commands.append("Z") self._lastCommand = "Z" self._lastX = self._lastY = None def _endPath(self): """ >>> pen = SVGPathPen(None) >>> pen.endPath() >>> pen._commands ['Z'] """ self._closePath() self._lastCommand = None self._lastX = self._lastY = None def getCommands(self): return "".join(self._commands) class SVGPen(DrawablePenMixin, SVGPathPen): def __init__(self, dat, h): super().__init__(None) self.defs = [] self.uses = [] self.dat = dat self.h = h tp = TransformPen(self, (1, 0, 0, -1, 0, h)) dat.round_to(0.1).replay(tp) def _endPath(self): """ >>> pen = SVGPathPen(None) >>> pen.endPath() >>> pen._commands ['Z'] """ #self._closePath() self._lastCommand = None self._lastX = self._lastY = None def fill(self, color): if color: if isinstance(color, Gradient): self.path.set("fill", f"url('#{self.gradient(color)}')") elif isinstance(color, Color): self.path.set("fill", self.rgba(color)) else: self.path.set("fill", "transparent") def stroke(self, weight=1, color=None, dash=None, miter=None): self.path.set("stroke-width", str(weight)) if dash: self.path.set("stroke-dasharray", " ".join([str(x) for x in dash])) if color: if isinstance(color, Gradient): self.path.set("stroke", f"url('#{self.gradient(color)}')") elif isinstance(color, Color): self.path.set("stroke", self.rgba(color)) else: self.path.set("stroke-width", 0) self.path.set("stroke", "transparent") def rgba(self, color): r, g, b, a = color.ints() return f"rgba({r}, {g}, {b}, {a})" def rect(self, rect): r = etree.Element("rect") fr = rect.flip(self.h) r.set("x", str(fr.x)) r.set("y", str(fr.y)) r.set("width", str(fr.w)) r.set("height", str(fr.h)) return r def shadow(self, clip=None, radius=10, alpha=0.3, color=Color.from_rgb(0,0,0,1)): hsh = str(hash(self.getCommands())) + str(randint(0, 1000000)) f = etree.Element("filter") f.set("x", "0") f.set("y", "0") f.set("width", "1000%") f.set("height", "1000%") f.set("x", "-500%") f.set("y", "-500%") f.set("id", f"shadow-{hsh}") fe = etree.Element("feDropShadow") fe.set("dx", "0") fe.set("dy", "0") fe.set("stdDeviation", str(radius)) #fe.set("slope", str(alpha)) fe.set("flood-color", self.rgba(color)) fe.set("flood-opacity", str(alpha)) f.append(fe) self.defs.append(f) if clip: cp = etree.Element("clipPath") cp.set("id", f"clip-path_{hsh}") cp.append(self.rect(clip)) self.defs.append(cp) #self.path.set("fill", "rgba(0, 0, 0, 0.3)") self.path.set("filter", f"url(#{f.get('id')})") if clip: self.path.set("clip-path", f"url(#clip-path_{hsh})") def gradient(self, gradient): lg = etree.Element("linearGradient") lg.set("id", f"gradient-{hash(self.getCommands())}-{randint(0, 100000)}") if gradient.stops[1][1].x == gradient.stops[0][1].x: lg.set("gradientTransform", "rotate(90)") s1 = etree.Element("stop", offset="0%") s1.set("stop-color", self.rgba(gradient.stops[0][0])) s2 = etree.Element("stop", offset="100%") s2.set("stop-color", self.rgba(gradient.stops[1][0])) lg.append(s1) lg.append(s2) self.defs.append(lg) return lg.get("id") def image(self, src=None, opacity=None, rect=None): if True: src = base64.b64encode(open(src, "rb").read()).decode('utf-8') hsh = str(hash(self.getCommands())) + str(randint(0, 100000)) img = etree.Element("image") img.set("x", str(rect.x or 0)) img.set("y", str(rect.y or 0)) img.set("width", str(rect.w or 100)) img.set("height", str(rect.h or 100)) img.set("opacity", str(opacity)) img.set("image-href", f"data:image/png;base64,{src}") pattern = etree.Element("pattern") pattern.set("x", img.get("x")) pattern.set("y", img.get("y")) pattern.set("width", img.get("width")) pattern.set("height", img.get("height")) pattern.set("patternUnits", "userSpaceOnUse") pattern.set("id", f"pattern-{hsh}") pattern.append(img) self.defs.append(pattern) self.path.set("fill", f"url(#pattern-{hsh})") def asSVG(self, style=None): self.path = etree.Element("path") for attrs, attr in self.findStyledAttrs(style): self.applyDATAttribute(attrs, attr) self.path.set("d", self.getCommands()) tag = self.dat.tag() if tag: self.path.set("data-tag", tag) g = etree.Element("g") defs = etree.Element("defs") for d in self.defs: defs.append(d) g.append(defs) g.append(self.path) for u in self.uses: g.append(u) return g def Composite(pens, rect, offset=False, style=None, viewBox=False, wrap="svg", to_string=True, ): docroot = etree.Element(wrap) if wrap == "svg": docroot.set("xmlns", "http://www.w3.org/2000/svg") if not viewBox: docroot.set("width", str(rect.w)) docroot.set("height", str(rect.h)) else: docroot.set("viewBox", f"0 0 {rect.w} {rect.h}") docroot.set("width", "100%") if offset: docroot.set("style", f"left:{rect.x}px;bottom:{rect.y}px;") for pen in SVGPen.FindPens(pens): sp = SVGPen(pen, rect.h) docroot.append(sp.asSVG(style=style)) if to_string: return etree.tostring(docroot).decode("utf-8").replace("image-href", "xlink:href") else: return docroot def Animation(frames, rect, fps): docroot = etree.Element("svg") docroot.set("xmlns", "http://www.w3.org/2000/svg") docroot.set("width", str(rect.w)) docroot.set("height", str(rect.h)) docroot.set("data-fps", str(fps)) docroot.set("data-duration", str(len(frames))) for idx, frame in enumerate(frames): g = SVGPen.Composite(frame, rect, wrap="g", to_string=False) g.set("id", f"frame_{idx}") g.set("class", "frame") docroot.append(g) return etree.tostring(docroot).decode("utf-8").replace("image-href", "xlink:href") if __name__ == "__main__": from pathlib import Path from coldtype.renderable.animation import animation, Action from coldtype.text import StSt, Font, Rect def show_animation(a:animation): idxs = range(0, a.duration+1) passes = a.passes(Action.PreviewIndices, None, idxs) results = [a.run_normal(rp) for rp in passes] Path("test.svg").write_text( SVGPen.Animation(results, a.rect, a.timeline.fps)) @animation(Rect(540, 540/2), timeline=30) def a1(f): return (StSt("CDELOPTY", Font.MutatorSans(), 50, wdth=f.e("eeio", 1), wght=f.e("seio", 1)) .align(f.a.r)) show_animation(a1) ================================================ FILE: packages/coldtype-core/src/coldtype/pens/translationpen.py ================================================ #coding=utf-8 from __future__ import division from math import pi, radians, degrees, tan, hypot, atan2, cos, sin """ Custom Robofab pens — Loïc Sander, june 2015. The MIT License (MIT) Copyright (c) 2014 Loïc Sander 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. """ from fontTools.pens.pointPen import PointToSegmentPen, SegmentToPointPen, ReverseContourPointPen from fontTools.misc.bezierTools import splitCubicAtT from fontTools.pens.basePen import BasePen _ANGLE_EPSILON = pi/36 def calcVector(point1, point2): x1, y1 = point1 x2, y2 = point2 dx = x2 - x1 dy = y2 - y1 return dx, dy def calcAngle(point1, point2): dx, dy = calcVector(point1, point2) return atan2(dy, dx) def polarCoord(xy, angle, distance): x, y = xy nx = x + (distance * cos(angle)) ny = y + (distance * sin(angle)) return nx, ny def calcArea(points): l = len(points) area = 0 for i in range(l): x1, y1 = points[i] x2, y2 = points[(i+1)%l] area += (x1*y2)-(x2*y1) return area / 2 def firstDerivative(xy1, cxy1, cxy2, xy2, value): x1, y1 = xy1 cx1, cy1 = cxy1 cx2, cy2 = cxy2 x2, y2 = xy2 mx = bezierTangent(x1, cx1, cx2, x2, value) my = bezierTangent(y1, cy1, cy2, y2, value) return mx, my def bezierTangent(a, b, c, d, t): # Implementation of http://stackoverflow.com/questions/4089443/find-the-tangent-of-a-point-on-a-cubic-bezier-curve-on-an-iphone return (-3*(1-t)**2 * a) + (3*(1-t)**2 * b) - (6*t*(1-t) * b) - (3*t**2 * c) + (6*t*(1-t) * c) + (3*t**2 * d) class TranslationPen(BasePen): """ Draw an outline resulting from the reunion of an initial contour and a translated version thereof. Translation is defined by an angle and a width/length. This kind of drawing basically produces a calligraphic effect (in a translated manner as Gerrit Noordzij puts it), it can also serve as a way of extruding a shape for 3D shadow effects. """ def __init__(self, otherPen, frontAngle=0, frontWidth=20): self.otherPen = otherPen self.frontAngle = radians(frontAngle) self.offset = polarCoord((0, 0), radians(frontAngle), frontWidth) self.points = [] def _moveTo(self, pt): self.points.append((pt, 'move')) def _lineTo(self, pt1): pt0, previousType = self.points[-1] angle = calcAngle(pt0, pt1) self.translatedLineSegment(pt0, pt1) self.points.append((pt1, 'line')) def _curveToOne(self, c1, c2, pt1): pt0, previousType = self.points[-1] newSegments = self.splitAtAngledExtremas(pt0, c1, c2, pt1) if len(newSegments): for segment in newSegments: pt0, c1, c2, pt1 = segment self.translatedCurveSegment(pt0, c1, c2, pt1) else: self.translatedCurveSegment(pt0, c1, c2, pt1) self.points.append((c1, None)) self.points.append((c2, None)) self.points.append((pt1, 'curve')) def endPath(self): self.points = [] def closePath(self): previousPoint, previousType = self.points[-1] if previousType in ['line','curve']: pt0, pt1 = self.points[-1][0], self.points[0][0] self.translatedLineSegment(pt0, pt1) self.points = [] def splitAtAngledExtremas(self, pt0, pt1, pt2, pt3): frontAngle = self.frontAngle segments = [] for i in range(101): t = i / 100 nx, ny = firstDerivative(pt0, pt1, pt2, pt3, t) tanAngle = atan2(ny, nx) if tan(frontAngle - _ANGLE_EPSILON) < tan(tanAngle) < tan(frontAngle + _ANGLE_EPSILON): newSegments = splitCubicAtT(pt0, pt1, pt2, pt3, t) if len(newSegments) > 1: segments = newSegments break return segments def translatedCurveSegment(self, pt0, c1, c2, pt1): ox, oy = self.offset x0, y0 = pt0 xc1, yc1 = c1 xc2, yc2 = c2 x1, y1 = pt1 pen = self.getPen([(x0, y0), (x1, y1), (x1+ox, y1+oy), (x0+ox, y0+oy)]) pen.moveTo((x0, y0)) pen.curveTo((xc1, yc1), (xc2, yc2), (x1, y1)) pen.lineTo((x1+ox, y1+oy)) pen.curveTo((xc2+ox, yc2+oy), (xc1+ox, yc1+oy), (x0+ox, y0+oy)) pen.closePath() def translatedLineSegment(self, pt0, pt1): ox, oy = self.offset x0, y0 = pt0 x1, y1 = pt1 pen = self.getPen([(x0, y0), (x1, y1), (x1+ox, y1+oy), (x0+ox, y0+oy)]) pen.moveTo((x0, y0)) pen.lineTo((x1, y1)) pen.lineTo((x1+ox, y1+oy)) pen.lineTo((x0+ox, y0+oy)) pen.closePath() def getPen(self, points): area = calcArea(points) if area < 0: pen = self.getReversePen() else: pen = self.otherPen return pen def getReversePen(self): adapterPen = PointToSegmentPen(self.otherPen) reversePen = ReverseContourPointPen(adapterPen) return SegmentToPointPen(reversePen) def addComponent(self, baseGlyphName, transformation): self.otherPen.addComponent(baseGlyphName, transformation) ================================================ FILE: packages/coldtype-core/src/coldtype/physics/pymunk.py ================================================ import pymunk from coldtype.runon.path import P def polygon(samples=5): def _polygon(p:P): pts = list(map(lambda x: x.pt.xy(), p.pen().samples(samples))) pmass = 3.0 pmoment = pymunk.moment_for_poly(pmass, pts) pbody = pymunk.Body(pmass, pmoment) pshape = pymunk.Poly(pbody, pts) pshape.friction = 0.01 pshape.elasticity = 0.9 pshape.collision_type = 0 return pbody, pshape return _polygon def segments(body, flatten=30): def _segments(p:P): p = p.copy().pen().removeOverlap() if flatten: p = p.flatten(flatten) segs = [] for line in p.segments(): p1, p2 = line.v.value[0][ 1][0], line.v.value[1][1][0] segs.append(pymunk.Segment(body, p1, p2, 0.0)) return segs return _segments ================================================ FILE: packages/coldtype-core/src/coldtype/random.py ================================================ from random import Random def random_series(start=0, end=1, seed=0, count=5000, ease=None, mod=None, spread=None): from coldtype.timing.easing import ez rnd = Random() rnd.seed(seed) rnds = [] for _ in range(count): tries = 0 while tries < 100: e = rnd.random() if ease: o = ez(e, ease, rng=(start, end)) else: o = start+rnd.random()*(end-start) if mod is not None: o = mod(o) if len(rnds) == 0: break if spread is None: break if abs(o-rnds[-1]) > spread: break tries += 1 rnds.append(o) return rnds ================================================ FILE: packages/coldtype-core/src/coldtype/raster.py ================================================ from coldtype.fx.skia import * from coldtype.img.skiaimage import SkiaImage try: from coldtype.fx.motion import filmjitter except: print("`pip install noise` for filmjitter") ================================================ FILE: packages/coldtype-core/src/coldtype/renderable/__init__.py ================================================ from coldtype.renderable.renderable import Overlay, Action, RenderPass, renderable, skia_direct, iconset, SkiaPen, ColdtypeCeaseConfigException, runnable from coldtype.renderable.animation import animation, aframe, FFMPEGExport, fontpreview from coldtype.renderable.ui import ui, UIState ================================================ FILE: packages/coldtype-core/src/coldtype/renderable/animation.py ================================================ import math, os, re, json from typing import Tuple from subprocess import run from pathlib import Path from datetime import datetime from coldtype.timing.timeable import Timeable from coldtype.timing import Frame from coldtype.timing.timeline import Timeline from coldtype.text.reader import Style, Font from coldtype.runon.path import P from coldtype.geometry import Rect, Point from coldtype.color import bw, hsl from coldtype.renderable.renderable import renderable, Action, RenderPass, Overlay from coldtype.osutil import show_in_finder def raw_gifski(width, fps, frames, output_path, open=False): """simpler wrapper for already-installed gifski""" run([ "gifski", "--fps", str(fps), "--width", str(width), "-o", output_path, *frames]) print("\n") if open: show_in_finder(output_path.parent) return True def gifski(a:"animation", passes, open=False): """simple wrapper for already-installed gifski""" root = a.pass_path(f"%4d.{a.fmt}").parent.parent gif = root / (a.name + ".gif") run([ "gifski", "--fps", str(a.timeline.fps), "--width", str(a.rect.w), "-o", gif, *[p.output_path for p in passes if p.render == a]]) print("\n") if open: show_in_finder(gif.parent) return gif class animation(renderable, Timeable): """ Base class for any frame-wise animation animatable by Coldtype """ def __init__(self, rect=(1080, 1080), timeline:Timeline=10, show_frame=True, offset=0, overlay=True, audio=None, suffixer=None, clip_cursor=True, reset_to_zero=False, release=None, **kwargs ): if "tl" in kwargs: timeline = kwargs["tl"] del kwargs["tl"] super().__init__(**kwargs) self.rect = Rect(rect).round() self.r = self.rect self.overlay = overlay self.start = 0 self.offset = offset self.show_frame = show_frame self.reset_timeline(timeline) self.single_frame = self.duration == 1 self.audio = audio self.suffixer = suffixer self.clip_cursor = clip_cursor self.reset_to_zero = reset_to_zero self.release = release def __call__(self, func): res = super().__call__(func) self.prefix = self.name + "_" return res def reset_timeline(self, timeline): if timeline is None: raise Exception("timeline= cannot be None") if not isinstance(timeline, Timeline): try: timeline = Timeline(timeline[0], fps=timeline[1]) except: timeline = Timeline(timeline) self.timeline = timeline self.t = timeline self.start = timeline.start self.end = timeline.end def folder(self, filepath): return filepath.stem + "/" + self.name # TODO necessary? def all_frames(self): return list(range(0, self.duration)) def _active_frames(self, renderer_state): frames = [] if renderer_state: frames.append((renderer_state.frame_offset + self.offset) % self.duration) return frames def active_frames(self, action, renderer_state, indices): if not action: return indices frames = self._active_frames(renderer_state) if action == Action.RenderAll: frames = self.all_frames() elif action in [Action.PreviewIndices, Action.RenderIndices]: frames = indices elif action in [Action.RenderWorkarea]: if self.timeline: try: frames = self.workarea() except: frames = self.all_frames() #if hasattr(self.timeline, "find_workarea"): # frames = self.timeline.find_workarea() #else: # frames = indices return frames def workarea(self): if hasattr(self.timeline, "workarea"): return list(self.timeline.workarea) elif hasattr(self.timeline, "workareas"): return list(self.timeline.workareas[0]) else: return list(range(0, self.duration)) def jump(self, current, direction): c = current % self.duration js = self.timeline.jumps() if direction < 0: for j in reversed(js): if c > j: return j else: for j in js: if c < j: return j return current def pass_suffix(self, index=0): idx = index % self.duration if self.suffixer: return self.suffixer(idx) elif self.suffix: pf = self.suffix + "_" else: pf = "" if isinstance(idx, int): return pf + "{:04d}".format(idx) else: return pf + index def render_and_rasterize(self, scale=1, style=None) -> str: first = self.render_and_rasterize_frame(0, scale=scale, style=style) for idx in range(1, self.timeline.duration): print(">>>", idx) self.render_and_rasterize_frame(idx, scale=scale, style=style) return first def passes(self, action, renderer_state, indices=[]): c, m = None, None rp = self.recording_path recording = None has_recording = False if rp and rp.exists(): has_recording = True recording = json.loads(rp.read_text()) if renderer_state: c = renderer_state.cursor if self.clip_cursor: c = c.clip(self.rect) m = renderer_state.midi if Overlay.Recording in renderer_state.overlays and self.overlay and rp: if not has_recording: recording = {"cursor":{}} frames = self.active_frames(action, renderer_state, indices) if renderer_state: if Overlay.Recording in renderer_state.overlays and self.overlay and rp: fi = frames[0] recording["cursor"][str(fi)] = [c.x, c.y] rp.write_text(json.dumps(recording)) return [RenderPass(self, action, i, [Frame(i, self, c, m, recording)]) for i in frames] def running_in_viewer(self): return True def run(self, render_pass, renderer_state, render_bg=True): fi = render_pass.args[0].i if renderer_state and self.running_in_viewer(): if renderer_state.previewing: if Overlay.Rendered in renderer_state.overlays: return self.frame_img(fi) self.t.hold(fi) return super().run(render_pass, renderer_state, render_bg=render_bg) @property def recording_path(self): try: return self.filepath.parent / (self.filepath.stem + "_recording.json") except: return None def runpost(self, result, render_pass, renderer_state, config): res = super().runpost(result, render_pass, renderer_state, config) if Overlay.Recording in renderer_state.overlays and self.overlay and self.recording_path: t = self.rect.take(50, "mny") frame:Frame = render_pass.args[0] res.append(P().rect(Point(0, 0).rect(150, 60)).t(10, 10).f(hsl(0.95, 1, 0.7))) res.append(P().text(f"{frame.i}", Style("Courier", 42, load_font=0, fill=bw(1)), t.inset(10))) if Overlay.Info in renderer_state.overlays and self.overlay: t = self.rect.take(50, "mny") frame:Frame = render_pass.args[0] return P([ res, P().rect(t).f(bw(0, 0.75)) if self.show_frame else None, P().text(f"{frame.i} / {self.duration}", Style("Times", 42, load_font=0, fill=bw(1)), t.inset(10)) if self.show_frame else None]) return res def package(self): pass def fn_to_frame(self, fn_name): return 0 def frame_to_fn(self, fi) -> Tuple[str, dict]: return None, {} def viewOffset(self, offset): @animation(self.rect, timeline=self.timeline, offset=offset, preview_only=1) def offset_view(f): return self.func(Frame(f.i, self)) return offset_view def contactsheet_renderable(self, scale=1, sl=slice(None, None, 1)): sheet = self.contactsheet(None, scale, sl) @renderable(sheet.data("frame"), bg=self.bg) def contactsheet(r): return sheet return contactsheet def contactsheet(self, r:Rect=None, scale=1, sl=slice(None, None, 1), border=True, grid=True, bgs=False): from coldtype.runon.scaffold import Scaffold xs = list(range(0, self.timeline.duration)[sl]) sq = math.ceil(math.sqrt(len(xs))) if r is None: r = Rect(0, 0, sq*self.rect.w*scale, sq*self.rect.h*scale) s = Scaffold(r).grid(sq, sq) def frame(x): try: fi = xs[x.i] return (self .frame_result(fi, frame=1, render_bg=bgs) .align(x.el.rect, tx=0) .scale(scale, tx=0)) except IndexError: return None return P( P(s.r).tag("border").fssw(-1, 0, 1) if border else None, s.borders().tag("grid").fssw(-1, 0, 1) if grid else None, P().enumerate(s, frame).tag("frames") ).data(frame=r) def frame_img(self, fi): from coldtype.img.skiaimage import SkiaImage return SkiaImage(self.pass_path(fi)) frameImg = frame_img def export(self, fmt, date=False, loops=1, open=1, audio=None, audio_loops=None, vf=None, set_709=True): def _export(passes): fe = FFMPEGExport(self, date=date, loops=loops, audio=audio or self.audio, audio_loops=audio_loops, vf=vf, set_709=set_709) if fmt == "gif": fe.gif() elif fmt == "h264": fe.h264() elif fmt == "prores": fe.prores() print(fe.args) fe.write() if open: fe.open() return fe return _export def gifski(self, open=False): def _gifski(passes): return gifski(self, passes, open=open) return _gifski class aframe(animation): def __init__(self, rect=(1080, 1080), **kwargs ): super().__init__(rect, timeline=Timeline(1), **kwargs) class FFMPEGExport(): def __init__(self, a:animation, date=False, loops=1, audio=None, audio_loops=None, output_folder=None, vf=None, set_709=True, ): self.a = a self.date = date self.loops = loops self.fmt = None self.failed = False self.set_709 = set_709 if audio: self.audio = Path(audio).expanduser() self.audio_loops = audio_loops if audio_loops is not None else loops if not self.audio.exists(): raise Exception("Audio file does not exist") else: self.audio = None self.audio_loops = loops template = a.pass_path(f"%4d.{a.fmt}") if output_folder is not None: self.output_folder = output_folder else: self.output_folder = template.parent.parent # https://github.com/typemytype/drawbot/blob/master/drawBot/context/tools/mp4Tools.py from coldtype.renderable.tools import FFMPEG_COMMAND print("> ffmpeg command ==", FFMPEG_COMMAND) self.args = [ FFMPEG_COMMAND, "-y", # overwrite existing files "-loglevel", "16", # 'error, 16' Show all errors "-r", str(self.a.timeline.fps) ] if self.set_709: set_709 = "zscale=matrixin=709:transferin=709:primariesin=709:matrix=709:transfer=709:primaries=709," else: set_709 = "" if self.audio: self.args.extend([ "-stream_loop", str(self.loops-1), "-i", template, # input sequence "-stream_loop", str(self.audio_loops-1), "-i", str(self.audio), #"-stream_loop", "-1", "-filter_complex", f"[0:v]loop=loop=0:size={self.a.timeline.duration}:start=0,{set_709}format=yuv420p[v]", "-map", '[v]', "-map", "1:a", ]) else: self.args.extend([ #"-stream_loop", str(self.loops-1), "-i", template, # input sequence "-filter_complex", f"[0:v]loop=loop={self.loops-1}:size={self.a.timeline.duration}:start=0,{set_709}format=yuv420p[v]", "-map", '[v]', ]) if vf: self.args.extend([ "-vf", vf ]) def h264(self): self.fmt = "mp4" self.args.extend([ "-c:v", "libx264", "-crf", "20", # Constant Rate Factor "-pix_fmt", "yuv420p", # pixel format #"", "", ]) return self def prores(self): # https://video.stackexchange.com/questions/14712/how-to-encode-apple-prores-on-windows-or-linux self.fmt = "mov" self.args.extend([ "-c:v", "prores_ks", "-c:a", "pcm_s16le", "-profile:v", "2" ]) return self def gif(self): self.fmt = "gif" #self.args.extend([]) return self def write(self, verbose=False, name=None): first_frame = self.a.pass_path(0) if not Path(first_frame).exists(): self.failed = True print("! Need to render before release") return self print(f"writing {self.fmt}...") if not self.fmt: raise Exception("No fmt specified") now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") d = ("_" + now) if self.date else "" if name is None: name = self.a.name self.output_path = self.output_folder / f"{name}{d}.{self.fmt}" self.args.append(self.output_path) if verbose: print(" ".join([str(s) for s in self.args])) run(self.args) if verbose: print(">", self.output_path) print("...done") return self def open(self): """i.e. Reveal-in-Finder""" if self.failed: return self show_in_finder(self.output_path.parent) return self class fontpreview(animation): def __init__(self, font_re, font_dir=None, rect=(1200, 150), limit=25, **kwargs): self.dir = font_dir self.re = font_re self.matches = [] for font in Font.List(self.re, self.dir): if re.search(self.re, str(font)): if len(self.matches) < limit: self.matches.append(font) self.matches.sort() super().__init__(rect=rect, timeline=Timeline(len(self.matches)), **kwargs) def passes(self, action, renderer_state, indices=[]): frames = self.active_frames(action, renderer_state, indices) return [RenderPass(self, action, i, [Frame(i, self), self.matches[i]]) for i in frames] import shutil try: from coldtype.img.skiaimage import SkiaImage except ImportError: SkiaImage = None class image_sequence(animation): """ Preview pre-rendered image-based animations; move images into renders folder with shutil.copy (good for something like making a prores file from an image sequence (if you use the FFMPEGExport)) """ def __init__(self, images, fps, looping=False, loops=1, **kwargs): self.images = images self.looping = looping if self.looping: timeline = Timeline(len(self.images)*2-2, fps) else: timeline = Timeline(len(self.images), fps) img = SkiaImage(self.images[0]) super().__init__(img.rect(), timeline, fmt=self.images[0].suffix[1:], **kwargs) self.self_rasterizing = True def normalize_result(self, pens): return pens def run(self, render_pass, renderer_state): from coldtype.timing.easing import ez idx = render_pass.idx if self.looping: t = idx/self.timeline.duration idx = round(ez(t, "l", 1, rng=(0, len(self.images)-1))) result = None if renderer_state and renderer_state.previewing: return self.images[idx] else: render_pass.output_path.parent.mkdir(exist_ok=True, parents=True) shutil.copy(self.images[idx], render_pass.output_path) result = render_pass.output_path return result try: import skia except ImportError: skia = None class skia_direct_animation(animation): def __init__(self, rect=(1080, 1080), **kwargs): super().__init__(rect=rect, direct_draw=True, **kwargs) def run(self, render_pass, renderer_state, canvas=None): if canvas is None: surface = skia.Surface(*self.rect.wh()) with surface as canvas: render_pass.fn(*render_pass.args, canvas) return if self.rstate: return render_pass.fn(*render_pass.args, renderer_state, canvas) else: return render_pass.fn(*render_pass.args, canvas) ================================================ FILE: packages/coldtype-core/src/coldtype/renderable/renderable.py ================================================ import inspect, platform, re, tempfile, math, datetime try: import skia from coldtype.pens.skiapen import SkiaPen from coldtype.pens.svgpen import SVGPen except ImportError: skia = None SkiaPen = None from enum import Enum from subprocess import run from pathlib import Path from coldtype.geometry import Rect, Point from coldtype.color import normalize_color from coldtype.text.reader import normalize_font_prefix, Font from coldtype.runon.path import P, Runon from coldtype.img.abstract import AbstractImage class Memory(object): def __init__(self, i, data) -> None: self.i = i self._keys = [] if data: for k, v in data.items(): self._keys.append(k) setattr(self, k, v) def add(self, k, v): if k not in self._keys: self._keys.append(k) setattr(self, k, v) return self def __eq__(self, other): equal = True for k in self.keys: if not equal: return False try: equal = getattr(self, k) == getattr(other, k) except: equal = False return equal class ColdtypeCeaseConfigException(Exception): pass class Overlay(Enum): Info = "info" Timeline = "timeline" Rendered = "rendered" Recording = "recording" class Action(Enum): Initial = "initial" Resave = "resave" RenderAll = "render_all" RenderWorkarea = "render_workarea" RenderIndices = "render_indices" Build = "build" Release = "release" PreviewStoryboard = "preview_storyboard" PreviewStoryboardReload = "preview_storyboard_reload" PreviewPlay = "preview_play" PreviewOnce = "preview_play" PreviewIndices = "preview_indices" PreviewStoryboardNext = "preview_storyboard_next" PreviewStoryboardPrev = "preview_storyboard_prev" PreviewStoryboardNextMany = "preview_storyboard_next_many" PreviewStoryboardPrevMany = "preview_storyboard_prev_many" ClearLastRender = "clear_last_render" ClearRenderedFrames = "clear_rendered_frames" RestartRenderer = "restart_renderer" Kill = "kill" class RenderPass(): def __init__(self, render:"renderable", action, idx, args): self.render = render self.action = action self.fn = self.render.func self.args = args self.path = None self.idx = idx self.prefix = render.pass_prefix() self.output_path = render.pass_path(index=idx) #self.output_path = render.output_folder / f"{self.prefix}{self.suffix}.{render.fmt}" self.i = None if hasattr(args[0], "i"): self.i = args[0].i def __repr__(self): return f"" class runnable(): """Minimal interface for runnable code in an abstract context (like a renderable but with nothing to render)""" def __init__(self, solo=False, cond=None): self.filepath = None self.codepath = None self.hidden = solo == -1 self.solo = solo self.preview_only = True self.render_only = False self.dst = None self.custom_folder = None self.name = None self.sort = 0 self.cv2caps = None self.watch = [] self.cond = cond def __call__(self, func): self.func = func if not self.name: self.name = self.func.__name__ return self def post_read(self): pass def run(self): return self.func() def folder(self, filepath): return filepath.stem + "/" + self.name class renderable(): """ Base class for any content renderable by Coldtype. """ def __init__(self, rect=(1080, 1080), bg=None, fmt="png", name=None, rasterizer=None, prefix=None, suffix=None, dst=None, custom_folder=None, post_preview=None, watch=[], watch_soft=[], watch_restart=[], solo=False, mute=False, rstate=False, preview_only=False, preview_scale=1, render_only=False, direct_draw=False, clip=False, composites=False, single_frame=True, interactable=False, cv2caps=None, render_bg=True, style="_default", viewBox=True, layer=False, cond=None, sort=0, hide=[], grid=None, xray=True, memory=None, reset_memory=None): """Base configuration for a renderable function""" self.rect = Rect(rect).round() self._stacked_rect = None if bg is not None and bg != -1: if callable(bg): self.bg_fn = bg self.bg = None else: self.bg_fn = None self.bg = normalize_color(bg) else: self.bg_fn = None self.bg = None self.fmt = fmt self.prefix = prefix self.suffix = suffix self.dst = Path(dst).expanduser().resolve() if dst else None self.custom_folder = custom_folder self.post_preview = post_preview self.last_passes = [] self.last_result = None self.last_return = None self.style = style self.composites = composites self.single_frame = single_frame self.interactable = interactable self.cv2caps = cv2caps self.grid = grid self.xray = xray self.memory = memory self.reset_memory = reset_memory self._hide = hide self.watch = [] for w in watch: self.add_watchee(w) self.watch_soft = [] for w in watch_soft: self.watch_soft.append(self.add_watchee(w, "soft")) self.watch_restart = [] for w in watch_restart: self.watch_restart.append(self.add_watchee(w, "restart")) self.cond = cond self.name = name self.codepath = None self.rasterizer = rasterizer self.self_rasterizing = False self.hidden = solo == -1 self.solo = solo self.mute = mute self.preview_only = preview_only self.preview_scale = preview_scale self.render_only = render_only self.rstate = rstate self.clip = clip self.viewBox = viewBox self.direct_draw = direct_draw self.render_bg = render_bg self.sort = sort self.layer = layer if self.layer: self.bg = normalize_color(None) self.filepath = None if not rasterizer: if self.fmt == "svg": self.rasterizer = "svg" elif self.fmt == "pickle": self.rasterizer = "pickle" else: self.rasterizer = "skia" def choose(self, fields): rec = {} for f in fields: if hasattr(self, f): rec[f] = getattr(self, f) return rec def post_read(self): pass def __repr__(self): return f"<{self.__class__.__name__}:{self.name}/>" def add_watchee(self, w, flag=None): try: pw = Path(w).expanduser().resolve() if not pw.exists(): print(w, "<<< does not exist (cannot be watched)") else: self.watch.append([pw, flag]) return pw except TypeError: if isinstance(w, Font): self.watch.append([w, flag]) else: raise Exception("Can only watch path strings, Paths, and Fonts") def __call__(self, func): self.func = func if not self.name: self.name = self.func.__name__ self.output_folder = Path(f"renders/{self.name}") return self def folder(self, filepath): return "" def pass_suffix(self, index=0): return self.name def pass_prefix(self): if self.prefix is None: if self.filepath is not None: prefix = f"{self.filepath.stem}_" else: prefix = None else: prefix = self.prefix return prefix def pass_path(self, index=0): if index is None: return self.output_folder / f"{self.pass_prefix()}" elif isinstance(index, int): return self.output_folder / f"{self.pass_prefix()}{self.pass_suffix(index)}.{self.fmt}" else: return self.output_folder / f"{self.pass_prefix()}{self.pass_suffix(index)}" def pass_img(self, index=0): from coldtype.img.skiaimage import SkiaImage return SkiaImage(self.pass_path(index=index)) def passes(self, action, renderer_state, indices=[]): return [RenderPass(self, action, 0, [self.rect])] def package(self): pass def write_reset_memory(self, renderer_state, new_memory, overwrite, initial): if initial and renderer_state and not renderer_state.memory_initial: renderer_state.memory_initial = Memory(0, self.memory) if not renderer_state or not self.memory: return if renderer_state.memory and not overwrite: if initial: mi = renderer_state.memory_initial for k, v in self.memory.items(): if not hasattr(mi, k) or getattr(mi, k) != v: renderer_state.memory_initial.add(k, v) renderer_state.memory.add(k, v) return if not new_memory: new_memory = self.memory i = 0 if renderer_state.memory and overwrite: i = renderer_state.memory.i if callable(new_memory): m = new_memory(i+1) else: m = new_memory renderer_state.memory = Memory(i+1, m) def run(self, render_pass, renderer_state, render_bg=True): self.write_reset_memory(renderer_state, self.memory, False, True) if self.rstate: res = render_pass.fn(*render_pass.args, renderer_state) elif self.memory: res = render_pass.fn(*render_pass.args, renderer_state.memory) else: res = render_pass.fn(*render_pass.args) if renderer_state: previewing = renderer_state.previewing else: previewing = False show_bg = (previewing or self.render_bg) and render_bg if show_bg: if self.bg_fn: if isinstance(self.bg_fn, type(self)): from coldtype.img.skiaimage import SkiaImage path = self.bg_fn.render_to_disk()[0] return P(SkiaImage(path), res) else: arg_count = len(inspect.signature(self.bg_fn).parameters) args = [self.rect, render_pass] return P([ self.bg_fn(*args[:arg_count]), res ]) elif self.bg: return P([ P(self.rect).f(self.bg), res ]) else: return P(res) else: return P(res) def show_xray(self, result): if not self.xray: return result from coldtype.fx.xray import skeleton, hsl out = P() def xray(p, pos, _): if pos == 0: out.append(p.copy().ch(skeleton(0.5))) result.copy().walk(xray) return P( result.copy().fssw(-1, hsl(0.95, 1, 0.8), 4), out.fssw(-1, hsl(0.65, 1, 0.6), 2)) def show_grid(self, result, settings): from coldtype.color import hsl, bw, Color invert_bg = self.bg.invert().with_alpha(0.5) grid = P().gridlines(self.rect).fssw(-1, invert_bg, 2) if self.grid is not None: if isinstance(self.grid, Color): g = {0:self.grid} else: g = {k:v for (k,v) in enumerate(self.grid)} grid = (P().gridlines(self.rect, g.get(2, 20), g.get(3, g.get(2, 20))) .fssw(-1, g.get(0, invert_bg), g.get(1, 2))) elif settings: g = {k:v for (k,v) in enumerate(settings)} grid = (P().gridlines(self.rect, g.get(2, 20), g.get(3, g.get(2, 20))) .fssw(-1, hsl(*g.get(0, invert_bg)), g.get(1, 2))) return P(result, grid) def runpost(self, result, render_pass:RenderPass, renderer_state, config): post_res = result if self.post_preview: post_res = self.post_preview(self, result) if config: if config.show_xray: post_res = self.show_xray(post_res) if config.show_grid: post_res = self.show_grid(post_res, config.grid_settings) return post_res def precompose(self, result, scale): from coldtype.fx.skia import precompose return result.ch(precompose(self.rect, scale=scale, style=self.style)) def postprocessor(self, result): if isinstance(result, Runon): has_post = result.find_(lambda el: el.data("postprocess") is not None, none_ok=True) if has_post: return has_post.data("postprocess") return None # def draw_preview(self, scale, canvas, rect, result, render_pass): # canvas:skia.Canvas # sr = self.rect.scale(scale, "mnx", "mxx") # SkiaPen.CompositeToCanvas(result, sr, canvas, scale, style=self.style) def noop(self, *args, **kwargs): return self def hide(self): self.hidden = True return self def _hide(self): self.hidden = False return self def show(self): self.hidden = False return self def _normalize_result(self, pens): if not pens: return P() elif not isinstance(pens, P): return P(pens) else: return pens def normalize_result(self, pens): #import inspect normalized = self._normalize_result(pens) #print(">norm", self, pens, normalized) #curframe = inspect.currentframe() #calframe = inspect.getouterframes(curframe, 2) #print('caller name:', calframe[1]) if self._hide: normalized.hide(*self._hide) return normalized def run_normal(self, render_pass, renderer_state=None, render_bg=True): return self.normalize_result( self.run(render_pass, renderer_state, render_bg=render_bg)) def frame_result(self, fi, post=False, frame=False, render_bg=True): p = self.passes(None, None, [fi])[0] res = self.run_normal(p, None, render_bg=render_bg) if post: res = self.runpost(res, p, None, None) if frame: res.data(frame=self.rect) return res def rasterize(self, config, content, render_pass): return False def render_and_rasterize_frame(self, frame, scale=1, style=None) -> str: SkiaPen.Composite(self.normalize_result(self.frame_result(frame)), self.rect, str(self.pass_path(frame)), scale=scale, context=None, style=style) return self.pass_path(frame) def render_and_rasterize(self, scale=1, style=None) -> str: return self.render_and_rasterize_frame(0, scale=scale, style=style) def render_to_disk(self, print_paths=False, return_base64=False, return_text=False, render_bg=False, return_img=False): passes = self.passes(Action.RenderAll, None) paths = [] for rp in passes: output_path = rp.output_path output_path.parent.mkdir(exist_ok=True, parents=True) if print_paths: print(output_path) result = self.run_normal(rp, None, render_bg) if self.fmt == "png": SkiaPen.Composite(result, self.rect, str(output_path), scale=1, context=None) elif self.fmt == "svg": output_path.write_text(SVGPen.Composite(result, self.rect, viewBox=self.viewBox)) elif self.fmt == "pdf": SkiaPen.PDFOnePage(result, self.rect, output_path, 1) else: print("render_to_disk", self.fmt, "not supported") paths.append(output_path) if return_img: from coldtype.img.skiaimage import SkiaImage return [SkiaImage(p) for p in paths] elif return_base64: from base64 import b64encode return [str(b64encode(p.read_bytes()), encoding="utf-8") for p in paths] elif return_text: return [p.read_text() for p in paths] else: return paths def _profile_render_all(self): ps = self.passes(Action.RenderAll, None) for p in ps: self.run_normal(p) return self def profile(self, file="profile.profile"): import cProfile cProfile.runctx(f"self._profile_render_all()", {}, {"self": self}, filename=file) class example(renderable): def __init__(self, rect=(800, 200), bg=1, **kwargs): super().__init__(rect=rect, bg=bg, **kwargs) class skia_direct(renderable): def __init__(self, rect=(1080, 1080), **kwargs): super().__init__(rect=rect, direct_draw=True, **kwargs) def run(self, render_pass, renderer_state, canvas=None): if canvas is None: surface = skia.Surface(*self.rect.wh()) with surface as canvas: render_pass.fn(*render_pass.args, canvas) return if self.rstate: return render_pass.fn(*render_pass.args, renderer_state, canvas) else: return render_pass.fn(*render_pass.args, canvas) # class glyph(renderable): # def __init__(self, glyphName, width=500, **kwargs): # r = Rect(kwargs.get("rect", Rect(1000, 1000))) # kwargs.pop("rect", None) # self.width = width # self.body = r.take(750, "mdy").take(self.width, "mdx") # self.glyphName = glyphName # super().__init__(rect=r, **kwargs) # def passes(self, action, renderer_state, indices=[]): # return [RenderPass(self, action, self.glyphName, [])] class iconset(renderable): valid_sizes = [16, 32, 64, 128, 256, 512, 1024] def __init__(self, sizes=[128, 1024], **kwargs): super().__init__(**kwargs) self.sizes = sizes def folder(self, filepath): return f"{filepath.stem}_source" def passes(self, action, renderer_state, indices=[]): # TODO could use the indices here sizes = self.sizes if action == Action.RenderAll: sizes = self.valid_sizes return [RenderPass(self, action, str(size), [self.rect, size]) for size in sizes] def package(self): # inspired by https://retifrav.github.io/blog/2018/10/09/macos-convert-png-to-icns/ iconset = self.output_folder.parent / f"{self.filepath.stem}.iconset" iconset.mkdir(parents=True, exist_ok=True) system = platform.system() if system == "Darwin": for png in self.output_folder.glob("*.png"): d = int(png.stem.split("_")[1]) for x in [1, 2]: if x == 2 and d == 16: continue elif x == 1: fn = f"icon_{d}x{d}.png" elif x == 2: fn = f"icon_{int(d/2)}x{int(d/2)}@2x.png" print(fn) run(["sips", "-z", str(d), str(d), str(png), "--out", str(iconset / fn)]) run(["iconutil", "-c", "icns", str(iconset)]) if True: # can be done windows or mac from PIL import Image output = self.output_folder.parent / f"{self.filepath.stem}.ico" largest = list(self.output_folder.glob("*_1024.png"))[0] img = Image.open(str(largest)) icon_sizes = [(x, x) for x in self.valid_sizes] img.save(str(output), sizes=icon_sizes) ================================================ FILE: packages/coldtype-core/src/coldtype/renderable/tools.py ================================================ from pathlib import Path FFMPEG_COMMAND = "ffmpeg" def set_ffmpeg_command(cmd): if isinstance(cmd, str) and "/" in cmd: cmd = str(Path(cmd).expanduser().absolute()) global FFMPEG_COMMAND FFMPEG_COMMAND = cmd ================================================ FILE: packages/coldtype-core/src/coldtype/renderable/ui.py ================================================ from coldtype.geometry.rect import Rect from coldtype.renderable.renderable import RenderPass, Overlay from coldtype.renderable.animation import animation, Frame class UIState(): def __init__(self, cursor, cursor_history, cursor_recording, midi, renderer_state, frame, ): self.c = cursor self.ch = cursor_history self.cr = cursor_recording self.f = frame self.i = frame.i self.r = frame.a.r self.midi = midi self.rs = renderer_state class ui(animation): def __init__(self, rect=Rect(1080, 1080), clip_cursor=True, cursor_recording={}, **kwargs ): if "preview_only" not in kwargs: kwargs["preview_only"] = True self.cursor_recording = cursor_recording super().__init__( rect=rect, #preview_only=True, interactable=True, clip_cursor=clip_cursor, **kwargs) def passes(self, action, renderer_state, indices=[]): c = renderer_state.cursor if self.clip_cursor: c = c.clip(self.rect) frames = self.active_frames( action, renderer_state, indices) if Overlay.Recording in renderer_state.overlays: if len(frames) > 0: self.cursor_recording[frames[0]] = c return [RenderPass(self, action, i, [UIState(c, renderer_state.cursor_history, self.cursor_recording, renderer_state.midi, renderer_state, Frame(i, self))]) for i in frames] ================================================ FILE: packages/coldtype-core/src/coldtype/renderer/__init__.py ================================================ import traceback, argparse, json, math, inspect import sys, os, signal, tracemalloc, shutil, re import pickle import time as ptime from pathlib import Path from subprocess import Popen from typing import Tuple, List from random import shuffle, Random from functools import partial import coldtype from coldtype.helpers import * from coldtype.runon.path import P from coldtype.geometry import Rect, Point from coldtype.text.reader import Font from coldtype.pens.svgpen import SVGPen from coldtype.renderable.tools import set_ffmpeg_command from coldtype.renderer.config import ConfigOption from coldtype.renderer.reader import SourceReader, run_source from coldtype.renderer.state import RendererState from coldtype.renderer.winman import Winmans, WinmanGLFWSkiaBackground from coldtype.renderable import renderable, animation, Action, Overlay, runnable from coldtype.renderer.keyboard import KeyboardShortcut, LAYOUT_REMAPS from coldtype.osutil import show_in_finder from coldtype.img.abstract import AbstractImage try: import skia from coldtype.pens.skiapen import SkiaPen import coldtype.fx.skia as skfx except ImportError: skia = None SkiaPen = None try: import drawBot as db except ImportError: db = None from coldtype.renderer.utils import * _random = Random() try: import psutil process = psutil.Process(os.getpid()) except ImportError: process = None class Renderer(): def Argparser(name="coldtype", file=True, defaults={}, nargs=[]): parser = argparse.ArgumentParser(prog=name, formatter_class=argparse.ArgumentDefaultsHelpFormatter) if file: parser.add_argument("file", type=str, nargs="?", help="The source file for a coldtype render") parser.add_argument("inputs", nargs="*", help="Additional input files passed to renderable") for narg in nargs: parser.add_argument(narg[0], nargs="?", default=narg[1]) pargs = dict( version=parser.add_argument("-v", "--version", action="store_true", default=False, help="Display version"), save_renders=parser.add_argument("-sv", "--save-renders", action="store_true", default=False, help="Should the renderer create image artifacts?"), rasterizer=parser.add_argument("-r", "--rasterizer", type=str, default=None, choices=["drawbot", "cairo", "svg", "skia", "pickle"], help="Which rasterization engine should coldtype use to create artifacts?"), cpu_render=parser.add_argument("-cpu", "--cpu-render", action="store_true", default=False, help="Should final rasters be performed without a GPU context?"), scale=parser.add_argument("-s", "--scale", type=float, default=1.0, help="When save-renders is engaged, what scale should images be rasterized at? (Useful for up-rezing)"), all=parser.add_argument("-a", "--all", action="store_true", default=False, help="If rendering an animation, pass the -a flag to render all frames sequentially"), render_directory=parser.add_argument("-rd", "--render-directory", action="store_true", default=False, help="kick off KeyboardShortcut.RenderDirectory straightaway and quit"), render_and_release=parser.add_argument("-rar", "--render-and-release", action="store_true", default=False, help="kick off KeyboardShortcut.RenderAndRelease straightaway and quit"), test_directory=parser.add_argument("-td", "--test-directory", action="store_true", default=False), build=parser.add_argument("-b", "--build", action="store_true", default=False, help="Should the build function be run and the renderer quit immediately?"), release=parser.add_argument("-rls", "--release", action="store_true", default=False, help="Should the release function be run and the renderer quit immediately?"), memory=parser.add_argument("-mm", "--memory", action="store_true", default=False, help="Show statistics about memory usage?"), is_subprocess=parser.add_argument("-isp", "--is-subprocess", action="store_true", default=False, help=argparse.SUPPRESS), config=parser.add_argument("-c", "--config", type=str, default=None, help="By default, Coldtype looks for a .coldtype.py file in ~ and the cwd; use this to override that and look at a specific file instead"), profile=parser.add_argument("-p", "--profile", type=str, default=None, help="What config profile do you want to use? Default is no profile"), cprofile=parser.add_argument("-cp", "--c-profile", action="store_true", default=False), format=parser.add_argument("-fmt", "--format", type=str, default=None, help="What image format should be saved to disk?"), indices=parser.add_argument("-i", "--indices", type=str, default=None), output_folder=parser.add_argument("-of", "--output-folder", type=str, default=None, help="If you don’t want to render to the default output location, specify that here."), show_exit_code=parser.add_argument("-sec", "--show-exit-code", action="store_true", default=False, help=argparse.SUPPRESS), frame_offset=parser.add_argument("-fo", "--frame-offset", type=int, default=0, help=argparse.SUPPRESS), #viewer_solos=parser.add_argument("-vs", "--viewer-solos", type=str, default=None, help=argparse.SUPPRESS), last_cursor=parser.add_argument("-lc", "--last-cursor", type=str, default="0,0", help=argparse.SUPPRESS), k=parser.add_argument("-k", "--k", type=str, default=None, help=argparse.SUPPRESS), never_reuse_skia_context=parser.add_argument("-nrsc", "--never-reuse-skia-context", action="store_true", default=False, help=argparse.SUPPRESS), print_skia_version=parser.add_argument("-psv", "--print-skia-version", action="store_true", default=False, help=argparse.SUPPRESS), ) ConfigOption.AddCommandLineArgs(pargs, parser) return pargs, parser def __init__(self, parser, winmans_class=Winmans, profile=None): sys.path.insert(0, os.getcwd()) self.subprocesses = {} if isinstance(parser, argparse.Namespace): self.args = parser else: self.args = parser.parse_args() if profile is not None: self.args.profile = profile if self.args.file == None: self.args.file = "." if self.args.file == "tools": import ast, textwrap for tool, file in SourceReader.Tools().items(): print(f"🔧 “{tool}”") if tool == "chars" or True: tree = ast.parse(file.read_text()) try: print(textwrap.indent(ast.get_docstring(tree), " ")) except: pass if False: print(" > Arguments:") for node in ast.walk(tree): if (isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "parse_inputs"): for keyword in node.args[1].keywords: print(" ", keyword.arg, "->", ast.unparse(keyword.value)) print("") self.dead = True return if self.args.version: print(coldtype.__version__) self.dead = True return self._unnormalized_file = self.args.file normalized = self.prenormalize_filepath(self.args.file) if normalized == -1: self.dead = True return self.args.file = str(normalized) if self.on_args_parsed(): self.dead = True return self._original_inputs = [*self.args.inputs] self.source_reader = SourceReader( renderer=self, inputs=self.args.inputs, cli_args=self.args) self.on_load_reader(self.source_reader) self.winmans = None self.winmans = winmans_class(self, self.source_reader.config) self.state = RendererState(self) self.watchees = [] self.rasterizer_warning = None self.needs_new_context = False self.print_result_once = False self.extent = None if not self.reset_filepath(self.args.file if hasattr(self.args, "file") else None): self.dead = True return else: self.dead = False self.state.preview_scale = self.source_reader.config.preview_scale self.state.inputs = self.args.inputs self.exit_code = 0 self.last_renders = [] self.last_render_cleared = False set_ffmpeg_command(self.source_reader.config.ffmpeg_command) # for multiplex mode self.running_renderers = [] self.completed_renderers = [] self.action_waiting = None self.action_waiting_reason = None self.actions_queued = [] self.debounced_actions = {} self.previews_waiting = [] self.last_animation = None self.last_animations = [] self.hotkeys = None self.hotkey_waiting = None self.stop_at_end = False self.viewer_solos = self.source_reader.config.viewer_solos self.viewer_sample_frames = 1 self.viewer_playback_rate = 1 def on_args_parsed(self): pass def on_load_reader(self, source_reader): pass def prenormalize_filepath(self, filepath): script = SourceReader.Script(filepath) if script: print(">>>", script) sr = SourceReader(renderer=self, inputs=self.args.inputs, cli_args=self.args) run_source(script, script, {}, {}, None, reader=sr) return -1 else: return SourceReader.Demo(filepath) def reset_filepath(self, filepath, reload=False): dirdirection = 0 if isinstance(filepath, int): dirdirection = filepath filepath = self.source_reader.filepath for _, cv2cap in self.state.cv2caps.items(): cv2cap.release() self.state.frame_offset = 0 self.state.cv2caps = {} root = Path(__file__).parent.parent filepath = self.source_reader.normalize_filepath(filepath) if not filepath.exists(): if filepath.suffix == ".py": print(">>> That python file does not exist...") create = input(">>> Do you want to create it and add some coldtype boilerplate? (y/n | a/r): ") if create.lower() in ["y", "a"]: filepath.parent.mkdir(exist_ok=True, parents=True) filepath.write_text((root / "demo/boiler.py").read_text()) self.open_in_editor(filepath) elif create.lower() in ["r"]: filepath.parent.mkdir(exist_ok=True, parents=True) filepath.write_text((root / "demo/boiler_renderable.py").read_text()) self.open_in_editor(filepath) else: raise Exception("That file does not exist") self._codepath_offset = 0 filepath = self.source_reader.reset_filepath(filepath, reload=False, dirdirection=dirdirection) try: printed = self.source_reader.print_docstring() except: printed = False # TODO check exists here on filepath self.watchees = [] self.add_watchee([Watchable.Source, self.source_reader.filepath, None]) ph = path_hash(self.source_reader.filepath) self.add_watchee([Watchable.Generic, Path(f"~/.coldtype/{ph}_input.json").expanduser(), None]) if reload: self.reload_and_render(Action.Initial) self.actions_queued.append(Action.PreviewStoryboardReload) self.winmans.set_title(filepath.name) # TODO close an open blend file? return True def watchee_paths(self): return [w[1] for w in self.watchees] def print_error(self): stack = traceback.format_exc() print(stack) return stack.split("\n")[-2] def renderable_error(self, rect): short_error = self.print_error() r = rect render = renderable(r) res = P([ P().rect(r).f(coldtype.Gradient.V(r, coldtype.hsl(_random.random(), l=0.3), coldtype.hsl(_random.random(), l=0.3)))]) render.show_error = short_error return render, res def show_error(self): if self.state.playing > 0: self.state.playing = -1 if (self.source_reader.config.no_viewer_errors or self.source_reader.config.no_viewer ): short_error = self.print_error() bc_print(bcolors.FAIL, "SYNTAX ERROR") bc_print(bcolors.WARNING, short_error) else: render, res = self.renderable_error(self.extent) render._stacked_rect = self.extent self.previews_waiting.append([render, res, None]) def show_message(self, message, scale=1): print(message) def play_sound(self, sound_name): if not self.source_reader.config.no_sound: play_sound(sound_name) def reload(self, trigger): if self.winmans.glsk: if self.args.never_reuse_skia_context: print("... not reusing SKIA_CONTEXT ...") else: skfx.SKIA_CONTEXT = self.winmans.glsk.context self.last_animations = [] if trigger == Action.Initial: self.state.frame_offset = self.args.frame_offset ui = dict( monitor=self.winmans.glsk.primary_monitor_rect if self.winmans.glsk and self.winmans.glsk.primary_monitor else None) if True: self.state.reset() self.source_reader.reload( output_folder_override=self.args.output_folder , initial=trigger==Action.Initial , restart_count=self.source_reader.config.restart_count , ui=ui) if trigger == Action.Initial and self.source_reader.config.restart_count == 0: if "__initials__" in self.source_reader.program: initials = self.source_reader.program["__initials__"]() for attr, setting in initials.items(): if attr == "config": for k, v in setting.items(): setattr(self.source_reader.config, k, v) else: setattr(self.state, attr, setting) self.winmans.did_reload(self.source_reader.filepath, self.source_reader) try: for r in self.renderables(Action.PreviewStoryboardReload): if isinstance(r, animation): if not r.preview_only and not r.render_only: self.last_animation = r self.last_animations.append(r) if trigger == Action.Initial: if hasattr(r, "initial"): r.initial() if self.last_animation: self.winmans.did_reload_animation(self.last_animation) if trigger == Action.Initial: if self.winmans.b3d: self.winmans.b3d.launch(self.source_reader.blender_io()) except SystemExit: self.on_exit(restart=False) return True except Exception as e: self.show_error() if self.last_animation: if self.last_animation.reset_to_zero: self.state.frame_offset = 0 ci = self.source_reader.config.cron_interval if ci > 0: self.winmans.cron_start = ptime.time() self.winmans.cron_interval = ci def animation(self): renderables = self.renderables(Action.PreviewStoryboard) for r in renderables: if isinstance(r, animation): return r def buildrelease_fn(self, fnname="release"): if not self.source_reader.program: return [] candidate = None for k, v in self.source_reader.program.items(): if k == fnname: candidate = v return candidate def normalize_fmt(self, render): if isinstance(render, runnable): return if self.args.format: render.fmt = self.args.format if self.args.rasterizer: render.rasterizer = self.args.rasterizer if render.rasterizer == "skia" and render.fmt in ["png", "pdf"] and skia is None: if not self.rasterizer_warning: self.rasterizer_warning = True print(f"RENDERER> SVG (skia-python not installed)") render.rasterizer = "svg" render.fmt = "svg" elif render.rasterizer == "drawbot" and render.fmt in ["png", "pdf"] and db is None: if not self.rasterizer_warning: self.rasterizer_warning = True print(f"RENDERER> SVG (no drawbot)") render.rasterizer = "svg" render.fmt = "svg" def renderables(self, trigger, previewing=False): _rs = self.source_reader.renderables( viewer_solos=self.viewer_solos, class_filters=[], previewing=previewing) if not _rs: return [] for r in _rs: self.normalize_fmt(r) caps = r.cv2caps if caps is not None: import cv2 for cap in caps: if cap not in self.state.cv2caps: self.state.cv2caps[cap] = cv2.VideoCapture(cap) return _rs def calculate_window_size(self, rs:List[renderable]): overall_preview_scale = self.state.preview_scale w = 0 llh = -1 lh = -1 h = 0 for r in rs: if not hasattr(r, "rect"): continue local_preview_scale = r.preview_scale * overall_preview_scale sr = r.rect.scale(local_preview_scale, "mnx", "mny").round() w = max(sr.w, w) adjr = None if r.layer: adjr = Rect(0, llh, sr.w, sr.h) else: adjr = Rect(0, lh+1, sr.w, sr.h) llh = lh+1 lh += sr.h + 1 h += sr.h + 1 r._stacked_rect = adjr.round() h -= 1 extent = Rect(0, 0, w, h) if not self.extent: needs_new_context = True else: needs_new_context = self.extent != extent self.extent = extent #if needs_new_context: # self.winmans.did_reset_extent(self.extent) return needs_new_context def _single_thread_render(self, trigger, indices=[], output_transform=None, no_sound=False, ditto_last=False) -> Tuple[int, int]: if not self.args.is_subprocess: start = ptime.time() previewing = (trigger in [ Action.Initial, Action.Resave, Action.PreviewStoryboard, Action.PreviewIndices, Action.PreviewStoryboardReload, ]) rendering = (self.args.save_renders or trigger in [ Action.RenderAll, Action.RenderWorkarea, Action.RenderIndices, ]) if len(self.previews_waiting) > 0 and not rendering: return 0, 0, [], [] def check_watches(render): for watch, flag in render.watch: if isinstance(watch, Font) and not watch.cacheable: if watch.path not in self.watchee_paths(): self.add_watchee([Watchable.Font, watch.path, flag]) for ext in watch.font.getExternalFiles(): if ext not in self.watchee_paths(): self.add_watchee([Watchable.Font, ext, flag]) elif watch not in self.watchee_paths(): self.add_watchee([Watchable.Generic, watch, flag]) if previewing and self.source_reader.config.load_only: renders = self.renderables(trigger) for r in renders: check_watches(r) return 0, 0, 0, [] if previewing and not self.source_reader.config.load_only: if Overlay.Rendered in self.state.overlays: overlays = [] overlay_count = 0 for render in self.last_renders: if render.preview_only: continue overlays.append(render) passes = render.passes(trigger, self.state, indices) render.last_passes = passes result = render.pass_path(passes[0].i) self.previews_waiting.append([ render, result, passes[0] ]) overlay_count += 1 return overlay_count, 0, overlays, [] self.state.previewing = previewing prev_renders = self.last_renders renders = self.renderables(trigger, previewing) self.last_renders = renders preview_count = 0 render_count = 0 collected_passes = [] try: for render in renders: if isinstance(render, runnable): render.run() continue check_watches(render) passes = render.passes(trigger, self.state, indices) render.last_passes = passes for rp in passes: collected_passes.append(rp) output_path = rp.output_path if output_transform: output_path = output_transform(output_path) if rendering and render.preview_only: continue try: if render.direct_draw: result = None else: # repopulate last_result across a save if not render.last_result: if len(prev_renders) > 0: for pr in prev_renders: if pr.name == render.name and pr.last_result and pr.composites: render.last_result = pr.last_result if render.single_frame and not render.interactable and render.last_result: result = render.last_result else: result = render.run(rp, self.state) if not isinstance(result, AbstractImage): render.last_return = result if not result and not render.direct_draw: #print(">>> No result") result = P().rect(render.rect).f(None) if previewing: if render.direct_draw: self.previews_waiting.append([render, None, rp]) else: if render.single_frame and not render.interactable and render.last_result: preview_count += 1 self.previews_waiting.append([render, result, rp]) else: if self.print_result_once or self.source_reader.config.print_result: self.print_result_once = False print("-"*90) print("@", ptime.time()) result.print() print("\n" + "-"*90) preview_result = render.normalize_result(render.runpost(result, rp, self.state, self.source_reader.config)) preview_count += 1 if preview_result: self.previews_waiting.append([render, preview_result, rp]) if rendering: if False: pass else: if render.preview_only: continue render_count += 1 output_path.parent.mkdir(exist_ok=True, parents=True) if render.self_rasterizing: try: print(">>> self-rasterized...", output_path.relative_to(Path.cwd())) except ValueError: print(">>> self-rasterized...", output_path) else: if render.direct_draw: show_render = self.rasterize(partial(render.run, rp, self.state), render, output_path, rp) else: show_render = self.rasterize(result or P(), render, output_path, rp) # TODO a progress bar? if show_render: try: print("") except ValueError: print(">>> saved...", str(output_path)) if ditto_last: copy_to = output_path.parent.parent / re.sub(r"\_[0-9]{4}\.png", "_last_render.png", output_path.name) print(">>>", output_path.name, re.sub(r"\_[0-9]{4}\.png", "_last_render.png", output_path.name)) shutil.copy2(output_path, copy_to) except Exception as e: #print(type(e)) self.show_error() except: self.show_error() if not self.args.is_subprocess and not previewing: print(f"") if len(self.actions_queued) > 0: no_sound = True if not previewing and not no_sound: if self.args.is_subprocess: self.play_sound("Morse") else: self.play_sound("Pop") return preview_count, render_count, renders, collected_passes def render(self, trigger, indices=[], ditto_last=False) -> Tuple[int, int]: #print(">RENDER!", trigger, self.action_waiting_reason)#, traceback.print_stack()) if self.args.is_subprocess: if trigger != Action.RenderIndices: raise Exception("Invalid child process render action", trigger) else: p, r, _, _ = self._single_thread_render(trigger, indices=indices) self.exit_code = 5 # mark as child-process return p, r elif self.source_reader.config.multiplex and self.animation(): if trigger in [Action.RenderAll, Action.RenderWorkarea]: all_frames = self.animation().all_frames() if trigger == Action.RenderAll: frames = all_frames elif trigger == Action.RenderWorkarea: anim = self.animation() frames = anim.workarea() if len(frames) == 0: frames = all_frames self.render_multiplexed(frames) trigger = Action.RenderIndices indices = [0, all_frames[-1]] # always render first & last from main, to trigger a filesystem-change detection in premiere #elif self.animation() and trigger == Action.RenderWorkarea: #all_frames = self.animation().all_frames() #self._single_thread_render(Action.RenderIndices, [0, all_frames[-1]]) preview_count, render_count, renders, passes = self._single_thread_render(trigger, indices, ditto_last=ditto_last) if not self.args.is_subprocess and render_count > 0: for render in renders: if hasattr(render, "package"): result = render.package() else: result = None if result: self.previews_waiting.append([render, result, None]) else: self.action_waiting = Action.PreviewStoryboard self.action_waiting_reason = "unclear" self.winmans.did_render(render_count, ditto_last, renders) did_render_fn = self.buildrelease_fn("didRender") if did_render_fn: did_render_fn(trigger, passes) if trigger == Action.RenderAll: did_render_all_fn = self.buildrelease_fn("didRenderAll") if did_render_all_fn: did_render_all_fn(passes) return preview_count, render_count def render_multiplexed(self, frames): start = ptime.time() tc = self.source_reader.config.thread_count print(f"") #group = math.floor(len(frames) / tc) ordered_frames = list(frames) #list(range(frames[0], frames[0]+len(frames))) shuffle(ordered_frames) import numpy as np subslices = np.array_split(ordered_frames, tc) #subslices = [list(s) for s in distribute(tc, ordered_frames)] self.reset_renderers() self.running_renderers = [] self.completed_renderers = [] for subslice in subslices: print(f"") if len(subslice) == 0: continue sargs = [ "coldtype", sys.argv[1], "-i", ",".join([str(s) for s in subslice]), "-isp", "-s", str(self.args.scale), ] if len(self.args.inputs) > 0: sargs = [*sargs[:2], *self.args.inputs, *sargs[2:]] if len(self.viewer_solos) > 0: sargs.append("-vs") sargs.append(",".join([str(x) for x in self.viewer_solos])) print(sargs) r = self.args.rasterizer if r: sargs.append("-r", r) if self.source_reader.config.no_sound: sargs.append("-ns", "1") if self.args.cpu_render or skia is None: sargs.append("-cpu") #print(sys.argv) #print(sargs) #return renderer = Popen(sargs) #stdout=log) self.running_renderers.append(renderer) while self.running_renderers.count(None) != len(self.running_renderers): for idx, renderer in enumerate(self.running_renderers): if renderer: retcode = renderer.poll() if retcode == 5: self.running_renderers[idx] = None ptime.sleep(.1) print(f"") self.play_sound("Frog") def rasterize(self, content, render, path, rp): if render.self_rasterizing: print("Self rasterizing") return True did_rasterize = render.rasterize(self.source_reader.config, content, rp) if did_rasterize: return False scale = int(self.args.scale) rasterizer = self.args.rasterizer or render.rasterizer if rasterizer == "drawbot": from coldtype.pens.rendererdrawbotpen import RendererDrawBotPen RendererDrawBotPen.Composite(content, render.rect, str(path), scale=scale) elif rasterizer == "skia": if not skia: raise Exception("pip install skia-python") if render.fmt == "png": postprocess = render.postprocessor(content) if render.composites or postprocess: from coldtype.img.skiaimage import SkiaImage content = content.ch(skfx.precompose(render.rect, scale=scale)) render.last_result = SkiaImage(content.img().get("src")) ctx = None if self.winmans.glsk and self.winmans.glsk.context and not self.args.cpu_render: ctx = self.winmans.glsk.context if postprocess: content = render.precompose(postprocess(content), scale) SkiaPen.Composite(content, render.rect, str(path), scale=scale, context=ctx, style=render.style) elif render.fmt == "pdf": SkiaPen.PDFOnePage(content, render.rect, str(path), scale=scale) elif render.fmt == "svg": SkiaPen.SVG(content, render.rect, str(path), scale=scale) else: print("> Skia render not supported for ", render.fmt) elif rasterizer == "svg": path.write_text(SVGPen.Composite(content, render.rect, viewBox=render.viewBox)) elif rasterizer == "pickle": pickle.dump(content, open(path, "wb")) else: raise Exception(f"rasterizer ({rasterizer}) not supported") return True def reload_and_render(self, trigger, watchable=None, indices=None): if (self.winmans.bg and not self.args.cpu_render and not self.winmans.glsk and not self.source_reader.config.no_viewer ): self.winmans.glsk = WinmanGLFWSkiaBackground(self.source_reader.config, self) wl = len(self.watchees) self.winmans.reset() try: should_halt = self.reload(trigger) if should_halt: return True if self.source_reader.program: renders = self.renderables(Action.Resave, previewing=True) self.needs_new_context = self.calculate_window_size(renders) if self.needs_new_context and trigger != Action.Initial: self.debounced_actions["reset_extent"] = ptime.time() #if self.needs_new_context: #and trigger != Action.Initial: # return True self.render(trigger, indices=indices) if self.state.playing < 0: self.state.playing = 1 else: print(">>>>>>>>>>>> No program loaded! <<<<<<<<<<<<<<") except: if not self.extent: self.extent = Rect(1200, 200) self.needs_new_context = True self.show_error() def main(self): self.profiler = None if self.args.c_profile: print(">>> profiling with cProfile...") import cProfile pr = cProfile.Profile() pr.enable() self.profiler = pr if self.dead: return if self.args.memory: tracemalloc.start(10) self._last_memory = -1 try: self.start() except KeyboardInterrupt: self.on_exit() if self.args.show_exit_code: print("exit>", self.exit_code) sys.exit(self.exit_code) def start(self): should_halt = self.before_start() if not self.winmans.bg: self.initialize_gui_and_server() if should_halt: self.on_exit() else: if self.args.all: self.reload_and_render(Action.RenderAll) if self.args.build: self.on_release(build=1) if self.args.release: self.on_release() elif self.args.build: self.reload_and_render(Action.RenderIndices, indices=[0]) self.on_release(build=1) if self.args.release: self.on_release() elif self.args.release: self.reload_and_render(Action.RenderIndices, indices=[0]) self.on_release() elif self.args.indices: indices = [int(x.strip()) for x in self.args.indices.split(",")] self.reload_and_render(Action.RenderIndices, indices=indices) else: should_halt = self.reload_and_render(Action.Initial) if should_halt: self.on_exit() return self.on_start() if not self.winmans.bg: if self.args.render_directory: self.actions_queued.append(KeyboardShortcut.RenderDirectory) if self.args.render_and_release: self.actions_queued.append(KeyboardShortcut.RenderAllAndRelease) self.actions_queued.append(KeyboardShortcut.Kill) if self.args.test_directory: self.actions_queued.append(KeyboardShortcut.TestDirectory) self.winmans.run_loop() else: self.on_exit() def before_start(self): pass def initialize_gui_and_server(self): self.winmans.add_viewers() if len(self.watchees) > 0: self.winmans.set_title(self.watchees[0][1].name) else: self.winmans.set_title("coldtype") kl = self.source_reader.config.keyboard_layout if kl is not None and kl not in LAYOUT_REMAPS: print(f"! -kl {kl} not recognized; will be ignored") print("valid kl options:", list(LAYOUT_REMAPS.keys())) self.hotkeys = None if self.source_reader.config.hotkeys: try: from pynput import keyboard mapping = {} for k, v in self.source_reader.config.hotkeys.items(): print("hotkey ::", k, v) mapping[k] = partial(self.on_hotkey, k, v) self.hotkeys = keyboard.GlobalHotKeys(mapping) self.hotkeys.start() except Exception as e: print(e) def on_start(self): pass def on_request_from_render(self, render, request, action=None): print("request (noop)>", render, request, action) def on_hotkey(self, key_combo, action): self.hotkey_waiting = (action, key_combo, None) def on_message(self, message, action): if action: enum_action = self.lookup_action(action) if enum_action: print(">", enum_action) self.action_waiting = enum_action self.action_waiting_reason = "on_message" #self.on_action(enum_action, message) else: print(">>> (", action, ") is not a recognized action") def jump_to_fn(self, fn_name): if self.last_animation: fi = self.last_animation.fn_to_frame(fn_name) if fi is None: print("fn_to_frame: no match") return False self.state.frame_offset = fi self.action_waiting = Action.PreviewStoryboard self.action_waiting_reason = "jump_to_fn" return True def lookup_action(self, action): return Action(action) def additional_actions(self): return [] def collect_passes(self): trigger = Action.RenderAll renders = self.renderables(trigger) all_passes = [] for render in renders: if not render.preview_only: all_passes.extend(render.passes(trigger, self.state, [0])) return all_passes def on_release(self, build=False, number=None): fnname = "build" if build else "release" if number is not None: fnname = "numpad" trigger = Action.RenderAll renders = self.renderables(trigger) attr_functions = [] for render in renders: if hasattr(render, fnname) and getattr(render, fnname): attr_functions.append([render, getattr(render, fnname)]) print(fnname, attr_functions[-1]) fn = self.buildrelease_fn(fnname) if not fn and not attr_functions: if fnname == "release" and self.last_animation: print("DEFAULT RELEASE == ffmpeg mp4") fn = self.last_animation.export("h264", audio=self.last_animation.audio) else: print(f"No `{fnname}` fn defined in source") return if attr_functions: #print(">>>>>>>>>>>", attr_functions) def _fn(passes): for render, af in attr_functions: res = af(render) # filter passes? if callable(res): res(passes) fn = _fn all_passes = self.collect_passes() try: if number is not None: fn = fn[number] arg_count = len(inspect.signature(fn).parameters) if arg_count == 0: res = fn() elif arg_count == 1: res = fn(all_passes) elif arg_count == 2: res = fn(all_passes, self) if isinstance(res, Action): return res except Exception as e: self.print_error() print("! Release failed !") print("/", fnname) def shortcut_to_action(self, shortcut): if shortcut == KeyboardShortcut.PreviewPrevMany: return Action.PreviewStoryboardPrevMany elif shortcut == KeyboardShortcut.PreviewPrev: return Action.PreviewStoryboardPrev elif shortcut == KeyboardShortcut.PreviewNextMany: return Action.PreviewStoryboardNextMany elif shortcut == KeyboardShortcut.PreviewNext: return Action.PreviewStoryboardNext elif shortcut == KeyboardShortcut.JumpHome: self.state.frame_offset = 0 elif shortcut == KeyboardShortcut.JumpEnd: self.state.frame_offset = -1 elif shortcut == KeyboardShortcut.JumpPrev: self.state.frame_offset = self.last_animation.jump(self.state.frame_offset, -1) elif shortcut == KeyboardShortcut.JumpNext: self.state.frame_offset = self.last_animation.jump(self.state.frame_offset, +1) elif shortcut == KeyboardShortcut.ClearLastRender: return Action.ClearLastRender elif shortcut == KeyboardShortcut.ClearRenderedFrames: return Action.ClearRenderedFrames elif shortcut == KeyboardShortcut.ResetInitialMemory: self.state.memory = None self.state.cursor_history = [] self.state.cursor = Point(0, 0) #self.last_animation.write_reset_memory(self.state, self.last_animation.memory, True) return Action.PreviewStoryboard elif shortcut == KeyboardShortcut.ResetMemory: self.last_animation.write_reset_memory(self.state, self.last_animation.reset_memory, True) return Action.PreviewStoryboard elif shortcut == KeyboardShortcut.PlayRendered: self.winmans.toggle_rendered() self.actions_queued = [Action.PreviewStoryboard] return Action.PreviewPlay elif shortcut == KeyboardShortcut.PlayPreview: return Action.PreviewPlay elif shortcut == KeyboardShortcut.PlayToEnd: self.stop_at_end = True return Action.PreviewPlay elif shortcut == KeyboardShortcut.Echo: self.play_sound("Pop") return Action.PreviewStoryboard elif shortcut == KeyboardShortcut.EnableAudio: if self.winmans.audio: self.source_reader.config.enable_audio = not self.source_reader.config.enable_audio self.winmans.mod_title("audio", self.source_reader.config.enable_audio) else: print('\n\n`pip install "coldtype[audio]"`\n\n') elif shortcut == KeyboardShortcut.ReloadSource: return Action.PreviewStoryboardReload elif shortcut == KeyboardShortcut.RestartApp: self.on_action(Action.RestartRenderer) return -1 elif shortcut == KeyboardShortcut.Kill: os.kill(os.getpid(), signal.SIGINT) os.system(f"kill {os.getpid()}") return -1 elif shortcut == KeyboardShortcut.Quit: self.dead = True return -1 elif shortcut == KeyboardShortcut.Release: self.on_action(Action.Release) return -1 elif shortcut == KeyboardShortcut.Build: self.on_action(Action.Build) return -1 elif shortcut == KeyboardShortcut.RenderAll: self.on_action(Action.RenderAll) return -1 elif shortcut == KeyboardShortcut.RenderAllAndPlay: self.on_action(Action.RenderAll) self.actions_queued.append(KeyboardShortcut.PlayRendered) return -1 elif shortcut == KeyboardShortcut.RenderAllAndRelease: vs = self.state.versions if vs: for v in vs: self.actions_queued.insert(0, KeyboardShortcut.CycleVersionsForward) self.actions_queued.insert(0, Action.Release) self.actions_queued.insert(0, Action.RenderAll) else: self.on_action(Action.RenderAll) self.actions_queued.insert(0, Action.Release) #self.action_waiting = Action.Release #self.action_waiting_reason = shortcut return -1 elif shortcut == KeyboardShortcut.RenderOne: la = self.last_animation self.on_action(Action.RenderIndices, [abs((self.state.frame_offset+la.offset) % la.duration)]) return -1 elif shortcut == KeyboardShortcut.RenderFrom: la = self.last_animation fo = abs((self.state.frame_offset+la.offset) % la.duration) idxs = list(range(fo, la.duration)) self.on_action(Action.RenderIndices, idxs) return -1 elif shortcut == KeyboardShortcut.RenderWorkarea: self.on_action(Action.RenderWorkarea) return -1 elif shortcut == KeyboardShortcut.ToggleMultiplex: self.source_reader.config.multiplex = not self.source_reader.config.multiplex print(f"") return -1 elif shortcut == KeyboardShortcut.OverlayInfo: self.state.toggle_overlay(Overlay.Info) elif shortcut == KeyboardShortcut.OverlayTimeline: self.state.toggle_overlay(Overlay.Timeline) elif shortcut == KeyboardShortcut.OverlayRecording: self.state.toggle_overlay(Overlay.Recording) elif shortcut == KeyboardShortcut.OverlayRendered: self.winmans.toggle_rendered() elif shortcut == KeyboardShortcut.ToggleTimeViewer: self.source_reader.config.add_time_viewers = not self.source_reader.config.add_time_viewers return Action.PreviewStoryboardReload elif shortcut == KeyboardShortcut.ToggleUI: self.source_reader.config.add_ui = not self.source_reader.config.add_ui return Action.PreviewStoryboardReload elif shortcut == KeyboardShortcut.ToggleXray: self.clear_last_render() self.source_reader.config.show_xray = not self.source_reader.config.show_xray return Action.PreviewStoryboard elif shortcut == KeyboardShortcut.ToggleGrid: self.clear_last_render() self.source_reader.config.show_grid = not self.source_reader.config.show_grid return Action.PreviewStoryboard elif shortcut == KeyboardShortcut.TogglePrintResult: self.clear_last_render() self.source_reader.config.print_result = not self.source_reader.config.print_result return Action.PreviewStoryboard elif shortcut == KeyboardShortcut.PrintResultOnce: self.clear_last_render() self.print_result_once = True return Action.PreviewStoryboard elif shortcut == KeyboardShortcut.PreviewScaleUp: return self.state.mod_preview_scale(+0.1) elif shortcut == KeyboardShortcut.PreviewScaleDown: return self.state.mod_preview_scale(-0.1) elif shortcut == KeyboardShortcut.PreviewScaleMin: return self.state.mod_preview_scale(0, 0.1) elif shortcut == KeyboardShortcut.PreviewScaleMax: return self.state.mod_preview_scale(0, 5) elif shortcut == KeyboardShortcut.PreviewScaleDefault: return self.state.mod_preview_scale(0, 1) elif shortcut == KeyboardShortcut.WindowOpacityDown: self.winmans.glsk.set_window_opacity(relative=-0.1) elif shortcut == KeyboardShortcut.WindowOpacityUp: self.winmans.glsk.set_window_opacity(relative=+0.1) elif shortcut == KeyboardShortcut.WindowOpacityMin: self.winmans.glsk.set_window_opacity(absolute=0.1) elif shortcut == KeyboardShortcut.WindowOpacityMax: self.winmans.glsk.set_window_opacity(absolute=1) elif shortcut == KeyboardShortcut.ViewerPlaybackSpeedIncrease: self.viewer_playback_rate = self.viewer_playback_rate * 2 elif shortcut == KeyboardShortcut.ViewerPlaybackSpeedDecrease: self.viewer_playback_rate = self.viewer_playback_rate / 2 elif shortcut == KeyboardShortcut.MIDIControllersPersist: self.state.persist() elif shortcut == KeyboardShortcut.MIDIControllersClear: self.state.clear() elif shortcut == KeyboardShortcut.MIDIControllersReset: self.state.reset(ignore_current_state=True) elif shortcut == KeyboardShortcut.JumpToFrameFunctionDef: frame = self.last_animation._active_frames(self.state)[0] fn_prefix, fn_context = self.last_animation.frame_to_fn(frame) original_code = self.source_reader.filepath.read_text().splitlines() found_line = -1 for li, line in enumerate(original_code): if line.strip().startswith(fn_prefix): found_line = li self.open_in_editor(line=found_line) elif shortcut == KeyboardShortcut.OpenInEditor: self.open_in_editor() elif shortcut == KeyboardShortcut.ShowInFinder: folder = self.renderables(Action.PreviewStoryboard)[-1].output_folder folder.mkdir(parents=True, exist_ok=True) show_in_finder(folder) elif shortcut == KeyboardShortcut.ViewerTakeFocus: self.winmans.glsk.focus(force=True) elif shortcut == KeyboardShortcut.ViewerSoloNone: self.viewer_solos = [] return Action.PreviewStoryboardReload elif shortcut == KeyboardShortcut.ViewerSoloNext: if len(self.viewer_solos): for i, solo in enumerate(self.viewer_solos): self.viewer_solos[i] = solo + 1 return Action.PreviewStoryboardReload elif shortcut == KeyboardShortcut.ViewerSoloPrev: if len(self.viewer_solos): for i, solo in enumerate(self.viewer_solos): self.viewer_solos[i] = solo - 1 return Action.PreviewStoryboardReload elif shortcut == KeyboardShortcut.ViewerSoloFirst: if len(self.viewer_solos): for i, solo in enumerate(self.viewer_solos): self.viewer_solos[i] = 0 return Action.PreviewStoryboardReload elif shortcut == KeyboardShortcut.ViewerSoloLast: if len(self.viewer_solos): for i, solo in enumerate(self.viewer_solos): self.viewer_solos[i] = -1 return Action.PreviewStoryboardReload elif shortcut in [ KeyboardShortcut.ViewerSolo1, KeyboardShortcut.ViewerSolo2, KeyboardShortcut.ViewerSolo3, KeyboardShortcut.ViewerSolo4, KeyboardShortcut.ViewerSolo5, KeyboardShortcut.ViewerSolo6, KeyboardShortcut.ViewerSolo7, KeyboardShortcut.ViewerSolo8, KeyboardShortcut.ViewerSolo9 ]: self.viewer_solos = [int(str(shortcut)[-1])-1] return Action.PreviewStoryboardReload elif shortcut == KeyboardShortcut.PrintApproxFPS: self.winmans.print_approx_fps = True elif shortcut.value.startswith("viewer_sample_frames"): self.viewer_sample_frames = int(shortcut.value.split("_")[-1]) elif shortcut.value.startswith("viewer_numbered_action_"): action_number = int(shortcut.value.split("_")[-1]) return self.on_release(number=action_number) elif shortcut == KeyboardShortcut.CopySVGToClipboard: self.winmans.glsk.copy_previews_to_clipboard = True return Action.PreviewStoryboard elif shortcut == KeyboardShortcut.RenderDirectory: adjs = self.buildrelease_fn("adjacents") if not adjs: adjs = self.source_reader.adjacents() else: adjs = adjs() for idx in range(0, len(adjs)): self.actions_queued.append(KeyboardShortcut.RenderAllAndRelease) #self.actions_queued.append(KeyboardShortcut.Release) if idx < (len(adjs) - 1): self.actions_queued.append(KeyboardShortcut.LoadNextInDirectory) self.actions_queued.append(Action.PreviewStoryboardReload) #return Action.Kill self.actions_queued.append(Action.Kill) return Action.PreviewStoryboardReload elif shortcut in [KeyboardShortcut.CycleVersionsForward, KeyboardShortcut.CycleVersionsBack]: vi = self.source_reader.config.version_index versions = self.state.versions if shortcut == KeyboardShortcut.CycleVersionsForward: vi += 1 if vi >= len(versions): vi = 0 else: vi -= 1 if vi < 0: vi = len(versions) - 1 self.source_reader.config.version_index = vi return Action.PreviewStoryboardReload elif shortcut == KeyboardShortcut.Sleep: # just delays dequeuing next action by a frame return Action.PreviewStoryboard elif shortcut == KeyboardShortcut.TestDirectory: adjs = self.buildrelease_fn("adjacents") if not adjs: adjs = self.source_reader.adjacents() else: adjs = adjs() for _ in range(0, len(adjs)): self.actions_queued.append(KeyboardShortcut.LoadNextInDirectory) self.actions_queued.append(Action.PreviewStoryboardReload) for _ in range(0, self.source_reader.config.test_directory_delay): self.actions_queued.append(KeyboardShortcut.Sleep) self.actions_queued.append(KeyboardShortcut.Kill) return Action.PreviewStoryboardReload elif shortcut in [ KeyboardShortcut.LoadNextInDirectory, KeyboardShortcut.LoadPrevInDirectory, ]: self.args.frame_offset = 0 d = -1 if shortcut == KeyboardShortcut.LoadPrevInDirectory else +1 f = self.buildrelease_fn("adjacent") if f: res = f(d) if isinstance(res, Action): return res elif isinstance(res, Path) or isinstance(res, str): self.reset_filepath(res, reload=True) else: self.reset_filepath(d, reload=True) else: print(shortcut, "not recognized") def open_in_editor(self, filepath=None, line=None): if filepath is None: filepath = self.source_reader.filepath try: path = filepath.relative_to(Path.cwd()) except ValueError: path = filepath editor_cmd = self.source_reader.config.editor_command if editor_cmd: if editor_cmd == "code": if line is not None: os.system(editor_cmd + " -g " + str(path) + ":" + str(line)) else: os.system(editor_cmd + " -g " + str(path)) else: os.system(editor_cmd + " " + str(path)) def on_shortcut(self, shortcut): waiting = self.shortcut_to_action(shortcut) if waiting: if waiting != -1: self.action_waiting = waiting self.action_waiting_reason = shortcut else: self.action_waiting = Action.PreviewStoryboard self.action_waiting_reason = "shortcut_default" def on_stdin(self, stdin): cmd, *args = stdin.split(" ") self.hotkey_waiting = (cmd, None, args) def on_action(self, action, message=None) -> bool: if isinstance(action, KeyboardShortcut): self.on_shortcut(self.action_waiting) return True if action in [ Action.RenderAll, Action.RenderWorkarea, Action.PreviewStoryboardReload ]: self.reload_and_render(action) return True elif action in [Action.RenderIndices]: self.reload_and_render(action, indices=message) elif action in [Action.PreviewStoryboard]: self.render(Action.PreviewStoryboard) elif action in [ Action.PreviewStoryboardNextMany, Action.PreviewStoryboardPrevMany, Action.PreviewStoryboardNext, Action.PreviewStoryboardPrev, Action.PreviewPlay]: if action == Action.PreviewPlay: self.winmans.toggle_playback() if action == Action.PreviewStoryboardPrevMany: self.winmans.frame_offset(-self.source_reader.config.many_increment) elif action == Action.PreviewStoryboardPrev: self.winmans.frame_offset(-self.viewer_sample_frames) elif action == Action.PreviewStoryboardNextMany: self.winmans.frame_offset(+self.source_reader.config.many_increment) elif action == Action.PreviewStoryboardNext: self.winmans.frame_offset(+self.viewer_sample_frames) self.render(Action.PreviewStoryboard) elif action == Action.Build: self.action_waiting = self.on_release(build=True) self.action_waiting_reason = "build" elif action == Action.Release: self.action_waiting = self.on_release() self.action_waiting_reason = "release" self.actions_queued.append(KeyboardShortcut.ReloadSource) elif action == Action.RestartRenderer: self.on_exit(restart=True) elif action == Action.Kill: os.kill(os.getpid(), signal.SIGINT) #self.on_exit(restart=False) elif action == Action.ClearLastRender: self.clear_last_render() self.action_waiting = Action.PreviewStoryboard self.action_waiting_reason = "clear_last_render" elif action == Action.ClearRenderedFrames: for r in self.renderables(Action.PreviewStoryboard): shutil.rmtree(r.output_folder, ignore_errors=True) print("Deleted rendered version") else: return False def clear_last_render(self): self.last_render_cleared = True for r in self.renderables(Action.PreviewStoryboard): r.last_result = None def turn_over(self): if self.dead: self.on_exit() return if self.hotkey_waiting: self.execute_string_as_shortcut_or_action(*self.hotkey_waiting) self.hotkey_waiting = None now = ptime.time() if self.debounced_actions: now = ptime.time() for k, v in self.debounced_actions.items(): if v: if (now - v) > self.source_reader.config.debounce_time: #self.action_waiting = Action.PreviewStoryboardReload self.actions_queued.append(Action.PreviewStoryboardReload) #self.action_waiting_reason = "debouncing" self.debounced_actions[k] = None if self.action_waiting: #print("YES", self.action_waiting, self.action_waiting_reason) action_in = self.action_waiting self.on_action(self.action_waiting) if action_in != self.action_waiting: # TODO should be recursive? self.on_action(self.action_waiting) self.action_waiting = None self.action_waiting_reason = None if len(self.actions_queued) > 0: self.action_waiting = self.actions_queued.pop(0) self.action_waiting_reason = "pop_from_queue" did_preview = self.winmans.turn_over() did_preview_fn = self.buildrelease_fn("didPreview") if did_preview_fn: did_preview_fn() self.previews_waiting = [] self.last_render_cleared = False if self.state.playing > 0: self.on_action(Action.PreviewStoryboardNext) for idx, (_, wp, flag, last_mod) in enumerate(self.watchees): wp:Path = wp if wp.exists(): mtime = wp.stat().st_mtime if mtime > last_mod: try: print(f">>> resave: {wp.relative_to(Path.cwd())}") except: print(f">>> resave: {wp}") self.watchees[idx][-1] = ptime.time() self.on_modified(wp, flag) return False return did_preview def on_modified(self, path, flag): #path = Path(event.src_path) #print("\n\n\n---\nMOD", path, ptime.time()) actual_path = path if path.parent in self.watchee_paths(): actual_path = path path = path.parent if True: #or path in self.watchee_paths(): if path.suffix == ".json": if path.stem == "command" or "_input" in path.stem: data = json.loads(path.read_text()) if "action" in data: action = data.get("action") if "filepath" in data: path = data.get("filepath") if path != str(self.last_animation.filepath): print("IGNORING COMMAND") return self.hotkey_waiting = (action, None, data.get("args")) return try: json.loads(path.read_text()) except json.JSONDecodeError: print("Error decoding watched json", path) return #idx = self.watchee_paths().index(path) #wpath, wtype, wflag = self.watchees[idx] if flag == "soft": self.action_waiting = Action.PreviewStoryboard self.action_waiting_reason = "soft_watch" return if flag == "restart": self.restart() if self.args.memory and process: memory = bytesto(process.memory_info().rss) diff = memory - self._last_memory self._last_memory = memory print(">>> pid:{:d}/new:{:04.2f}MB/total:{:4.2f}".format(os.getpid(), diff, memory)) self.action_waiting = Action.PreviewStoryboardReload self.action_waiting_reason = "on_modified" def add_watchee(self, watchee): if self.args.is_subprocess: return watchee.append(ptime.time()) self.watchees.append(watchee) if "_input.json" in str(watchee[1]): return if False: try: print(" >>> watching...", watchee[1].relative_to(Path.cwd())) except Exception as e: #print(e) print(" >>> watching...", watchee[1]) def execute_string_as_shortcut_or_action(self, shortcut, key, args=[]): #print("\n>>> shortcut:", shortcut) #print(f" \"{shortcut}\"({key if key else 'ø'}[{args}])\n") co = ConfigOption.ShortToConfigOption(shortcut) if co: if co == ConfigOption.WindowOpacity: self.winmans.glsk.set_window_opacity(absolute=args[0]) else: print("> Unhandled ConfigOption", co, key) return if shortcut == "render_index": self.render(Action.RenderIndices, indices=[int(args[0])], ditto_last=True) return elif shortcut == "render_after": frames = list(range(args[0], self.last_animation.timeline.duration)) self.render(Action.RenderIndices, indices=frames) return elif shortcut == "render_scratch": fi = int(args[0]) print(f">/scratch:{fi}") def to_scratch(p): return Path(re.sub(r"[0-9]{4}", "XXXX", str(p))) self._single_thread_render(Action.RenderIndices, [fi], output_transform=to_scratch, no_sound=True) return elif shortcut == "custom_hotkey": custom_hotkey_fn = self.buildrelease_fn("custom_hotkey") if custom_hotkey_fn: custom_hotkey_fn(key, self) return try: ksc = KeyboardShortcut(shortcut) except ValueError: ksc = None if ksc: self.on_shortcut(KeyboardShortcut(shortcut)) else: print("No shortcut/action", key, shortcut) def reset_renderers(self): for r in self.running_renderers: if r: r.terminate() def restart(self): print("> RESTARTING...") args = sys.argv if len(args) > 1: args[1] = str(self.source_reader.filepath) #args[1] = str(self._unnormalized_file) inputs = self.source_reader.program["__inputs__"] if len(inputs) > 0: if len(self._original_inputs) == len(inputs): for idx, input in enumerate(inputs): args[idx+2] = input else: a_args = args[:2] b_args = args[len(self._original_inputs)+2:] args = a_args + inputs + b_args #print(">>>", a_args, "|||", b_args) # attempt to preserve state across reload fo = str(self.state.frame_offset) try: foi = args.index("-fo") args[foi+1] = fo except ValueError: args.append("-fo") args.append(fo) tv = str(int(self.source_reader.config.add_time_viewers or 0)) try: tvi = args.index("-tv") args[tvi+1] = tv except ValueError: args.append("-tv") args.append(tv) ui = str(int(self.source_reader.config.add_ui or 0)) try: uii = args.index("-ui") args[uii+1] = ui except ValueError: args.append("-ui") args.append(ui) x = str(int(self.source_reader.config.show_xray or 0)) try: xi = args.index("-x") args[xi+1] = x except ValueError: args.append("-x") args.append(x) g = str(int(self.source_reader.config.show_grid or 0)) try: gi = args.index("-g") args[gi+1] = g except ValueError: args.append("-g") args.append(g) vi = str(int(self.source_reader.config.version_index)) try: vii = args.index("-vi") args[vii+1] = vi except ValueError: args.append("-vi") args.append(vi) lc = [] for c in self.state.cursor_history[-3:]: lc.append(",".join([str(p) for p in c])) lc = ";".join(lc) try: lci = args.index("-lc") args[lci+1] = lc except ValueError: args.append("-lc") args.append(lc) rc = self.source_reader.config.restart_count + 1 args.append("-rc") args.append(rc) print("> RESTART:", args) os.execl(sys.executable, *(["-m"]+[str(a) for a in args])) def on_exit(self, restart=False): renderables = self.renderables(Action.PreviewStoryboard) for r in renderables: if hasattr(r, "exit"): r.exit() self.source_reader.unlink() exit_fn = self.buildrelease_fn("exit") if exit_fn: exit_fn(self) for _, p in self.subprocesses.items(): p.kill() self.winmans.terminate() if self.hotkeys: self.hotkeys.stop() self.reset_renderers() if self.args.memory: snapshot = tracemalloc.take_snapshot() top_stats = snapshot.statistics('traceback') # pick the biggest memory block stat = top_stats[0] print("%s memory blocks: %.1f KiB" % (stat.count, stat.size / 1024)) for line in stat.traceback.format(): print(line) if restart: self.restart() if self.profiler: print(">>>PROFILED!") print(self.profiler) self.profiler.disable() self.profiler.dump_stats("profile_result") def main(winmans=Winmans, profile=None): Path("~/.coldtype").expanduser().mkdir(exist_ok=True) _, parser = Renderer.Argparser() Renderer(parser, winmans_class=winmans, profile=profile).main() def main_b3d(): main(profile="b3dlo") if __name__ == "__main__": main() ================================================ FILE: packages/coldtype-core/src/coldtype/renderer/config.py ================================================ from enum import Enum import argparse, re, platform from pathlib import Path from coldtype.renderer.keyboard import LAYOUT_REMAPS def true_false_or_none(x): if x in ["0", "false", "False", "n", "no", "N"]: return False elif x in ["1", "true", "True", "y", "yes", "Y"]: return True else: return None class ConfigOption(Enum): WindowPassthrough = ("window_passthrough", None, "wpass", true_false_or_none) WindowTransparent = ("window_transparent", None, "wt", true_false_or_none) WindowChromeless = ("window_chromeless", None, "wc", true_false_or_none) WindowBackground = ("window_background", None, "wb", true_false_or_none) WindowFloat = ("window_float", None, "wf", true_false_or_none) WindowOpacity = ("window_opacity", 1, "wo", lambda x: float(x)) WindowPin = ("window_pin", "SE", "wp") WindowPinInset = ("window_pin_inset", (0, 0), "wpi", lambda x: [int(n) for n in x.split(",")]) WindowPinOffsetX = ("window_pin_offset_x", 0, "wpox", lambda x: int(x)) WindowPinOffsetY = ("window_pin_offset_y", 0, "wpoy", lambda x: int(x)) WindowContentScale = ("window_content_scale", None, "wcs", lambda x: float(x)) MonitorName = ("monitor_name", None, "mn") EditorCommand = ("editor_command", "code", "ec") ManyIncrement = ("many_increment", None, "minc") PreviewScale = ("preview_scale", 1, "ps", lambda x: float(x)) PreviewSaturation = ("preview_saturation", 1, "psat", lambda x: float(x)) FontDirs = ("font_dirs", [], "fd") FunctionFilters = ("function_filters", "", "ff", lambda x: [re.compile(f.strip()) for f in x.split(",")]) Midi = ("midi", {}, None) Hotkeys = ("hotkeys", {}, None) ThreadCount = ("thread_count", 8, "tc") Multiplex = ("multiplex", None, "mp", true_false_or_none) DebounceTime = ("debounce_time", 0.25, "dt") RefreshDelay = ("refresh_delay", 0.025, "rdly") InlineFiles = ("inline_files", [], "in", lambda x: x.split(",")) SrcMacros = ("src_macros", {}, "srcm") FFMPEGCommand = ("ffmpeg_command", "ffmpeg", "ffc") BlenderWatch = ("blender_watch", None, "bw", true_false_or_none) BlenderFile = ("blender_file", None, "bf", lambda x: Path(x).expanduser().resolve()) BlenderResetFactory = ("blender_reset_factory", None, "brf", true_false_or_none) BlenderCommandLineArgs = ("blender_command_line_args", None, "bcli") NoWatch = ("no_watch", None, "nw", true_false_or_none) NoViewer = ("no_viewer", None, "nv", true_false_or_none) NoMIDI = ("no_midi", None, "nm", true_false_or_none) MIDIInfo = ("midi_info", None, "mi", true_false_or_none) NoSound = ("no_sound", None, "ns", true_false_or_none) NoViewerErrors = ("no_viewer_errors", None, "nve", true_false_or_none) EnableAudio = ("enable_audio", None, "ea", true_false_or_none) ViewerSolos = ("viewer_solos", [], "vs", lambda x: [int(n) for n in x.split(",")]) AddTimeViewers = ("add_time_viewers", None, "tv", true_false_or_none) AddUI = ("add_ui", True, "ui", true_false_or_none) ShowXray = ("show_xray", None, "x", true_false_or_none) ShowGrid = ("show_grid", None, "g", true_false_or_none) GridSettings = ("grid_settings", [], "gs", lambda x: x.split(",")) PrintResult = ("print_result", None, "pr", true_false_or_none) LoadOnly = ("load_only", None, "lo", true_false_or_none) TestDirectoryDelay = ("test_directory_delay", 10, "tdd", lambda x: int(x)) VersionIndex = ("version_index", 0, "vi", lambda x: int(x)) RestartCount = ("restart_count", 0, "rc", lambda x: int(x)) CronInterval = ("cron_interval", 0, "ci", lambda x: float(x)) KeyboardLayout = ("keyboard_layout", None, "kl") @staticmethod def Help(e): if e == ConfigOption.WindowPassthrough: return "Should the window ignore all interaction?" elif e == ConfigOption.WindowTransparent: return "Should the window have no background?" elif e == ConfigOption.WindowChromeless: return "Should the window have no chrome?" elif e == ConfigOption.WindowBackground: return "Should the window open as a background process?" elif e == ConfigOption.WindowFloat: return "Should the window float above everything?" elif e == ConfigOption.WindowOpacity: return "A value for the transparency of the window" elif e == ConfigOption.WindowPin: return "Where should the window show up? Provide a compass direction N/S/E/W/NE/SE/SW/NW/C" elif e == ConfigOption.WindowPinInset: return "Experimental; offset window pin from edge" elif e == ConfigOption.WindowContentScale: return "Experimental; override auto-calculated window scale" elif e == ConfigOption.MonitorName: return "The name of the monitor to open the window in; pass 'list' to list all monitor names" elif e == ConfigOption.EditorCommand: return "If your text editor provides a command-line invocation, set it here for automated opening" elif e == ConfigOption.ManyIncrement: return "How many frames should the renderer jump forward when you hit cmd+arrow to move prev/next?" elif e == ConfigOption.PreviewScale: return "What preview scale should the window open at?" elif e == ConfigOption.PreviewSaturation: return "Should the preview have a saturation value applied to compensate for your screen?" elif e == ConfigOption.FontDirs: return "What additional directories would you like to search for fonts?" elif e == ConfigOption.FunctionFilters: return "Do you want to restrict renderable functions to those that match these comma-delimited patterns?" elif e == ConfigOption.ThreadCount: return "How many threads when multiplexing?" elif e == ConfigOption.Multiplex: return "Should the renderer run multiple processes (determined by --thread-count)?" elif e == ConfigOption.DebounceTime: return "How long should the rendering loop wait before acting on a debounced action?" elif e == ConfigOption.RefreshDelay: return "How long should the renderer delay between rendering frames?" elif e == ConfigOption.BlenderWatch: return "Enable experimental blender live-coding integration?" elif e == ConfigOption.BlenderResetFactory: return "Reset Blender to factory settings before running?" elif e == ConfigOption.NoViewer: return "Should there be no viewer at all?" elif e == ConfigOption.NoMIDI: return "Should MIDI be disabled?" elif e == ConfigOption.MIDIInfo: return "Should information about the current MIDI setup and messages be printed while the program runs?" elif e == ConfigOption.NoSound: return "Turn off all sounds made by the renderer" elif e == ConfigOption.NoViewerErrors: return "Turn off displaying errors in the viewer" elif e == ConfigOption.EnableAudio: return "Enable audio playback if audio is defined on an @animation" elif e == ConfigOption.AddTimeViewers: return "Begin with time-viewers visible?" elif e == ConfigOption.ShowXray: return "Show the Bezier xray instead of the thing itself?" elif e == ConfigOption.ShowGrid: return "Add a grid to the output?" elif e == ConfigOption.PrintResult: return "Print the result" elif e == ConfigOption.KeyboardLayout: return f"Remap keyboard for layout, options: {list(LAYOUT_REMAPS.keys())}" @staticmethod def AddCommandLineArgs(pargs:dict, parser:argparse.ArgumentParser ): for co in ConfigOption: if co.value[2]: short = co.value[2] arg = co.value[0].replace("_", "-") type_spec = dict(default=None) #if co.value[1] is False: # type_spec = dict(action="store_true", default=False) pargs[co.value[0]] = parser.add_argument(f"-{short}", f"--{arg}", help=ConfigOption.Help(co), **type_spec) @staticmethod def ShortToConfigOption(short): for co in ConfigOption: if short == co.value[2]: return co _set_paths = {} class ColdtypeConfig(): def __init__(self, config, prev_config:"ColdtypeConfig"=None, args=None ): global _set_paths self.profile = None if args and hasattr(args, "profile") and args.profile: self.profile = args.profile for co in ConfigOption: post_mod = None if len(co.value) > 4: prop, default_value, _, cli_mod, post_mod = co.value elif len(co.value) > 3: prop, default_value, _, cli_mod = co.value else: prop, default_value, _ = co.value cli_mod = lambda x: x if prop in _set_paths and prev_config: setattr(self, prop, getattr(prev_config, prop)) else: value = config.get(prop.upper(), getattr(prev_config, prop) if prev_config else default_value) setattr(self, prop, value) if self.profile and "PROFILES" in config and self.profile in config["PROFILES"]: v = config["PROFILES"][self.profile].get(prop.upper()) if v: _set_paths[prop] = "PROFILE" setattr(self, prop, v) if args and hasattr(args, prop): value = getattr(args, prop) if value is not None: #print("CLI", prop, value) setattr(self, prop, cli_mod(value)) if post_mod: setattr(self, prop, post_mod(getattr(self, prop))) def values(self): out = "": glfw.KEY_UP, "": glfw.KEY_DOWN, "": glfw.KEY_LEFT, "": glfw.KEY_RIGHT, "": glfw.KEY_SPACE, "": glfw.KEY_HOME, "": glfw.KEY_END, "": glfw.KEY_ENTER, "": glfw.KEY_PAGE_UP, "": glfw.KEY_PAGE_DOWN, "": glfw.KEY_TAB, ",": glfw.KEY_COMMA, ".": glfw.KEY_PERIOD, "-": glfw.KEY_MINUS, "=": glfw.KEY_EQUAL, "/": glfw.KEY_SLASH, "'": glfw.KEY_APOSTROPHE, ";": glfw.KEY_SEMICOLON, "": glfw.KEY_BACKSLASH, "": glfw.KEY_RIGHT_BRACKET, "": glfw.KEY_LEFT_BRACKET, } if s in GLFW_SPECIALS_LOOKUP: return GLFW_SPECIALS_LOOKUP[s] else: k = f"KEY_{s.upper()}" if hasattr(glfw, k): return getattr(glfw, k) elif "np" in s: return getattr(glfw, f"KEY_KP_{s[2:]}") else: raise Exception("Invalid keyboard shortcut symbol", s) SHORTCUTS = { KeyboardShortcut.PreviewPrevMany: [ [["shift"], "j"], [["shift"], ""] ], KeyboardShortcut.PreviewPrev: [ [[], "j"], [[], ""] ], KeyboardShortcut.PreviewNextMany: [ [["shift"], "l"], [["shift"], ""] ], KeyboardShortcut.PreviewNext: [ [[], "l"], [[], ""] ], KeyboardShortcut.ClearLastRender: [ [[], ""] ], KeyboardShortcut.ClearRenderedFrames: [ [["shift"], ""] ], KeyboardShortcut.ResetInitialMemory: [ [[], ""] ], KeyboardShortcut.ResetMemory: [ [[], ""] ], KeyboardShortcut.PlayRendered: [ [["shift"], ""], #[["shift"], glfw.KEY_K] ], KeyboardShortcut.PlayPreview: [ [[], ""], #[[], glfw.KEY_K] ], KeyboardShortcut.PlayToEnd: [ [[], "e"] ], KeyboardShortcut.Echo: [ [["shift"], "e"] ], KeyboardShortcut.EnableAudio: [ [[], "."] ], KeyboardShortcut.ReloadSource: [ [[], ""], ], KeyboardShortcut.RestartApp: [ [["cmd"], "r"] ], KeyboardShortcut.Quit: [ [[], "q"] ], KeyboardShortcut.Release: [ [[], "r"], ], KeyboardShortcut.Build: [ [[], "b"], ], KeyboardShortcut.RenderAll: [ [[], "a"], ], KeyboardShortcut.RenderAllAndPlay: [ [["shift"], "a"], ], KeyboardShortcut.RenderAllAndRelease: [ [["shift", "cmd"], "a"], ], KeyboardShortcut.RenderDirectory: [ [["shift", "cmd", "alt"], "a"], ], KeyboardShortcut.RenderOne: [ [["cmd"], "a"], ], KeyboardShortcut.RenderFrom: [ [["alt"], "a"], ], KeyboardShortcut.RenderWorkarea: [ [[], "w"] ], KeyboardShortcut.ToggleMultiplex: [ [[], "m"] ], KeyboardShortcut.SetWorkareaIn: [ [["cmd"], "i"] ], KeyboardShortcut.SetWorkareaOut: [ [["cmd"], "o"] ], KeyboardShortcut.JumpPrev: [ [[], ""], [[], "i"] ], KeyboardShortcut.JumpNext: [ [[], ""], [[], "k"] ], KeyboardShortcut.JumpHome: [ [[], ""], [["shift"], "h"] ], KeyboardShortcut.JumpEnd: [ [[], ""] ], KeyboardShortcut.JumpStoryboard: [ [["cmd"], ""] ], KeyboardShortcut.OverlayInfo: [ [[], "/"] ], KeyboardShortcut.OverlayTimeline: [ [["cmd"], "t"] ], KeyboardShortcut.OverlayRendered: [ [[], "'"], [[], ","], ], KeyboardShortcut.OverlayRecording: [ [[], ""], ], KeyboardShortcut.ToggleTimeViewer: [ [[], "v"], ], KeyboardShortcut.ToggleUI: [ [["shift"], "u"], ], KeyboardShortcut.ToggleXray: [ [[], "x"], ], KeyboardShortcut.ToggleGrid: [ [[], "g"], ], KeyboardShortcut.CycleVersionsForward: [ [["shift"], "v"], [["shift"], ""], ], KeyboardShortcut.CycleVersionsBack: [ [["shift"], ""], ], KeyboardShortcut.PreviewScaleUp: [ [[], "="] ], KeyboardShortcut.PreviewScaleDown: [ [[], "-"] ], KeyboardShortcut.PreviewScaleMin: [ [["cmd"], "-"] ], KeyboardShortcut.PreviewScaleMax: [ [["cmd"], "="] ], KeyboardShortcut.PreviewScaleDefault: [ [["cmd"], "0"] ], KeyboardShortcut.ViewerPlaybackSpeedDecrease: [ [["shift"], "-"] ], KeyboardShortcut.ViewerPlaybackSpeedIncrease: [ [["shift"], "="] ], # KeyboardShortcut.ViewerPlaybackSpeedReset: [ # [["shift"], "0"] # ], KeyboardShortcut.WindowOpacityDown: [ [["cmd", "shift"], ""] ], KeyboardShortcut.WindowOpacityUp: [ [["cmd", "shift"], ""] ], KeyboardShortcut.WindowOpacityMin: [ [["cmd", "shift", "alt"], ""], ], KeyboardShortcut.WindowOpacityMax: [ [["cmd", "shift", "alt"], ""] ], KeyboardShortcut.JumpToFrameFunctionDef: [ [["cmd"], "f"], ], KeyboardShortcut.OpenInEditor: [ [[], "o"] ], KeyboardShortcut.ShowInFinder: [ [[], "s"] ], KeyboardShortcut.ViewerSoloNone: [ [["shift"], "np0"], [["shift"], "0"] ], KeyboardShortcut.ViewerSoloNext: [ [["cmd"], ""] ], KeyboardShortcut.ViewerSoloPrev: [ [["cmd"], ""] ], KeyboardShortcut.ViewerSoloFirst: [ [["cmd"], ""] ], KeyboardShortcut.ViewerSoloLast: [ [["cmd"], ""] ], KeyboardShortcut.ViewerSolo1: [ [["shift"], "np1"], [["shift"], "1"] ], KeyboardShortcut.ViewerSolo2: [ [["shift"], "np2"], [["shift"], "2"] ], KeyboardShortcut.ViewerSolo3: [ [["shift"], "np3"], [["shift"], "3"] ], KeyboardShortcut.ViewerSolo4: [ [["shift"], "np4"], [["shift"], "4"] ], KeyboardShortcut.ViewerSolo5: [ [["shift"], "np5"], [["shift"], "5"] ], KeyboardShortcut.ViewerSolo6: [ [["shift"], "np6"], [["shift"], "6"] ], KeyboardShortcut.ViewerSolo7: [ [["shift"], "np7"], [["shift"], "7"] ], KeyboardShortcut.ViewerSolo8: [ [["shift"], "np8"], [["shift"], "8"] ], KeyboardShortcut.ViewerSolo9: [ [["shift"], "np9"], [["shift"], "9"] ], KeyboardShortcut.PrintApproxFPS: [ [[], "f"], ], KeyboardShortcut.ViewerSampleFrames1: [ [["cmd"], "np1"], [["cmd"], "1"] ], KeyboardShortcut.ViewerSampleFrames2: [ [["cmd"], "np2"], [["cmd"], "2"] ], KeyboardShortcut.ViewerSampleFrames3: [ [["cmd"], "np3"], [["cmd"], "3"] ], KeyboardShortcut.ViewerSampleFrames4: [ [["cmd"], "np4"], [["cmd"], "4"] ], KeyboardShortcut.ViewerSampleFrames5: [ [["cmd"], "np5"], [["cmd"], "5"] ], KeyboardShortcut.ViewerSampleFrames6: [ [["cmd"], "np6"], [["cmd"], "6"] ], KeyboardShortcut.ViewerSampleFrames7: [ [["cmd"], "np7"], [["cmd"], "7"] ], KeyboardShortcut.ViewerSampleFrames8: [ [["cmd"], "np8"], [["cmd"], "8"] ], KeyboardShortcut.ViewerSampleFrames9: [ [["cmd"], "np9"], [["cmd"], "9"] ], KeyboardShortcut.ViewerNumberedAction1: [ [[], "np1"], [[], "1"] ], KeyboardShortcut.ViewerNumberedAction2: [ [[], "np2"], [[], "2"] ], KeyboardShortcut.ViewerNumberedAction3: [ [[], "np3"], [[], "3"] ], KeyboardShortcut.ViewerNumberedAction4: [ [[], "np4"], [[], "4"] ], KeyboardShortcut.ViewerNumberedAction5: [ [[], "np5"], [[], "5"] ], KeyboardShortcut.ViewerNumberedAction6: [ [[], "np6"], [[], "6"] ], KeyboardShortcut.ViewerNumberedAction7: [ [[], "np7"], [[], "7"] ], KeyboardShortcut.ViewerNumberedAction8: [ [[], "np8"], [[], "8"] ], KeyboardShortcut.ViewerNumberedAction9: [ [[], "np9"], [[], "9"] ], KeyboardShortcut.ToggleCapturing: [ [["shift"], "c"] ], KeyboardShortcut.CaptureOnce: [ [["cmd", "shift"], "c"] ], KeyboardShortcut.CopySVGToClipboard: [ [["cmd"], "c"] ], KeyboardShortcut.TogglePrintResult: [ [["shift"], "p"] ], KeyboardShortcut.PrintResultOnce: [ [[], "p"] ], KeyboardShortcut.LoadNextInDirectory: [ [["cmd", "alt"], ""], [[], ""], [[], "u"], ], KeyboardShortcut.LoadPrevInDirectory: [ [["cmd", "alt"], ""], [[], ""], [[], "y"], ], KeyboardShortcut.TestDirectory: [ [["shift"], "t"] ], KeyboardShortcut.MIDIControllersPersist: [ [["cmd"], "m"], ], KeyboardShortcut.MIDIControllersClear: [ [["alt"], "m"], ], KeyboardShortcut.MIDIControllersReset: [ [["shift"], "m"], ] } def shortcuts_keyed(): keyed = {} for k, v in SHORTCUTS.items(): modded = [] for mods, symbol in v: modded.append([[symbol_to_glfw(s) for s in mods], symbol_to_glfw(symbol), symbol]) if "cmd" in mods: mod_mods = ["ctrl" if mod == "cmd" else mod for mod in mods] modded.append([[symbol_to_glfw(s) for s in mod_mods], symbol_to_glfw(symbol), symbol]) keyed[k] = modded return keyed if __name__ == "__main__": from subprocess import Popen, PIPE out = """ Cheatsheet ========== """ for shortcut, key_combos in list(SHORTCUTS.items())[:]: name = str(shortcut).split(".")[-1] explain = KeyboardShortcutExplainers.get(shortcut) if not explain: continue out += f"* **{name}** (`\"{KeyboardShortcut(shortcut).value}\"`)\n" out += ("\n " + explain + "\n\n") for mods, key in key_combos: kc = " ".join([*mods, key]) out += (" * " + f"`{kc}`\n") out += "\n\n" print(out) process = Popen('pbcopy', env={'LANG': 'en_US.UTF-8'}, stdin=PIPE) process.communicate(out.encode("utf-8")) ================================================ FILE: packages/coldtype-core/src/coldtype/renderer/reader.py ================================================ import re, os from pathlib import Path from runpy import run_path from functools import partial from tempfile import NamedTemporaryFile from subprocess import run from typing import Union from coldtype.blender import BlenderIO from coldtype.renderable import renderable, ColdtypeCeaseConfigException, runnable, animation, aframe, ui from coldtype.renderer.utils import Watchable, on_linux, on_mac, on_windows from coldtype.renderer.config import ColdtypeConfig, ConfigOption from coldtype.helpers import sibling from coldtype.text.reader import ALL_FONT_DIRS from coldtype.geometry.rect import Rect from coldtype.timing import Timeline from coldtype.timing.viewer import timeViewer from coldtype.renderer.ui import uiView try: from docutils.core import publish_doctree except ImportError: publish_doctree = None def apply_syntax_mods(filepath, source_code, renderer=None, source_reader=None): codepath_offset = 0 def inline_arg(p): path = Path(inline.strip()).expanduser().absolute() if renderer: if path not in renderer.watchee_paths(): renderer.add_watchee([Watchable.Source, path, None]) src = path.read_text() codepath_offset = len(src.split("\n")) return src def inline_other(x): cwd = Path.cwd() m = x.group(1) if m.startswith("."): path = filepath.parent / (m[1:] + ".py") else: path = Path(cwd / (m.replace(".", "/")+".py")) if renderer: if path not in renderer.watchee_paths(): renderer.add_watchee([Watchable.Source, path, None]) src = path.read_text() codepath_offset = len(src.split("\n")) return src if source_reader and source_reader.config.inline_files: for inline in source_reader.config.inline_files: source_code = inline_arg(inline) + "\n" + source_code source_code = re.sub(r"from ([^\s]+) import \* \#INLINE", inline_other, source_code) #source_code = re.sub(r"([(),\-]{1,})([0-9]{1,}\+)?[0-9]{1,}\.?[0-9]{0,}j", imperial, source_code) #source_code = re.sub(r"ℛ", "return ", source_code) #source_code = re.sub(r"\-\.[A-Za-z_ƒ]+([A-Za-z_0-9]+)?\(", ".nerp(", source_code) #source_code = re.sub(r"([\s]+)Ƨ\(", r"\1nerp(", source_code) #source_code = re.sub(r"λ[\s]{0,3}\.", "lambda p: p.", source_code) #source_code = re.sub(r"λ\s?([/\.\@]{1,2})", r"lambda xxx: xxx\1", source_code) #source_code = re.sub(r"ι,λ\.", "lambda ι, λ__: λ__.", source_code) source_code = re.sub(r"λ(\s+)?\.", "lambda λ__: λ__.", source_code) source_code = re.sub(r"λ__", "λ", source_code) source_code = re.sub(r"ºº", "__", source_code) #source_code = re.sub(r"_º", "__", source_code) source_code #source_code = re.sub(r"λ", "lambda ", source_code) #source_code = re.sub(r"ßDPS\(([^\)]+)\)", r"(ß:=P(\1))", source_code) source_code = re.sub(r"\.___([^_\(]+)\(", ".noop(", source_code) if source_reader and source_reader.config.src_macros: for pattern, src_macro in source_reader.config.src_macros.items(): if re.match(pattern, str(filepath)): source_code = src_macro(filepath, source_code) # while "nerp(" in source_code: # start = source_code.find("nerp(") # end = -1 # i = 5 # depth = 1 # c = source_code[start+i] # while depth > 0 and c: # #print(c, depth) # if c == "(": # depth += 1 # elif c== ")": # depth -= 1 # i += 1 # c = source_code[start+i] # end = start+i # source_code = source_code[:start] + "noop()" + source_code[end:] # #print(start, end) if renderer: renderer._codepath_offset = codepath_offset return source_code, codepath_offset def read_source_to_tempfile(filepath:Path, codepath:Path=None, renderer=None, source_reader=None, ): data_out = {} if filepath.suffix == ".rst": if not publish_doctree: raise Exception("pip install docutils") doctree = publish_doctree(filepath.read_text()) def is_code_block(node): if node.tagname == "literal_block": classes = node.attributes["classes"] # ok the "ruby" here is an ugly hack but it's so I can hide certain code # from a printed rst if "code" in classes and ("python" in classes or "ruby" in classes): return True return False code_blocks = doctree.traverse(condition=is_code_block) source_code = [block.astext() for block in code_blocks] if codepath and codepath.exists(): codepath.unlink() with NamedTemporaryFile("w", prefix="coldtype_rst_src", suffix=".py", delete=False) as tf: tf.write("\n".join(source_code)) codepath = Path(tf.name) elif filepath.suffix == ".md": from lxml.html import fragment_fromstring, tostring try: import markdown except ImportError: raise Exception("pip install markdown") try: import frontmatter except ImportError: frontmatter = None print("> pip install python-frontmatter") md = markdown.markdown(filepath.read_text(), extensions=["fenced_code"]) fm = frontmatter.loads(filepath.read_text()) data_out["frontmatter"] = fm frag = fragment_fromstring(md, create_parent=True) blocks = [] for python in frag.findall("./pre/code[@class='language-python']"): blocks.append(python.text) source_code = "\n".join(blocks) if codepath and codepath.exists(): codepath.unlink() with NamedTemporaryFile("w", prefix="coldtype_md_src", suffix=".py", delete=False) as tf: mod_src, _ = apply_syntax_mods(filepath, source_code, renderer, source_reader) tf.write(mod_src) codepath = Path(tf.name) elif filepath.suffix == ".py": source_code, _ = apply_syntax_mods(filepath, filepath.read_text(), renderer, source_reader) if codepath and codepath.exists(): codepath.unlink() with NamedTemporaryFile("w", prefix=f"coldtype__{filepath.stem}_", suffix=".py", delete=False) as tf: tf.write(source_code) codepath = Path(tf.name) else: raise Exception("No code found in file!") return codepath, data_out def run_source(filepath, codepath, inputs, memory, config, **kwargs): #print(">>>>>>>>>>>>>>>>>>>>>>>>>>>", str(codepath)) return run_path(str(codepath), init_globals={ "__COLDTYPE__": True, "__FILE__": filepath, "__inputs__": inputs, "__config__": config, "__memory__": memory, "__as_config__": False, "__ui__": kwargs.get("ui"), "__sibling__": partial(sibling, filepath), **kwargs}) def renderable_to_output_folder(filepath, renderable, override=None): if override: return Path(override).expanduser().resolve() elif renderable.dst: return renderable.dst / (renderable.custom_folder or renderable.folder(filepath)) else: return (filepath.parent if filepath else Path(os.getcwd())) / "renders" / (renderable.custom_folder or renderable.folder(filepath)) def find_renderables( filepath:Path, codepath:Path, program:dict, output_folder_override=None, blender_io=None, args=None, ): all_rs = [] filtered_rs = [] for k, v in program.items(): if (isinstance(v, renderable) or isinstance(v, runnable)) and not v.hidden: if v.cond is not None: if callable(v.cond) and not v.cond(): continue elif not v.cond: continue if v not in all_rs: all_rs.append(v) elif k == "RENDERABLES": for r in v: all_rs.append(r) #all_rs = sorted(all_rs, key=lambda r: r.layer) all_rs = sorted(all_rs, key=lambda r: r.sort) for r in all_rs: r.filepath = filepath r.codepath = codepath r.output_folder = renderable_to_output_folder( filepath, r, override=output_folder_override) if hasattr(r, "blender_io"): r.blender_io = blender_io r.post_read() if any([r.solo for r in all_rs]): filtered_rs = [r for r in all_rs if r.solo] else: filtered_rs = all_rs try: filtered_rs = [r for r in filtered_rs if not r.mute] except AttributeError: pass return filtered_rs def filter_renderables(filtered_rs, viewer_solos=[], function_filters=[], class_filters=[], previewing=False, ): if function_filters: function_patterns = function_filters matches = [] for r in filtered_rs: for fp in function_patterns: try: if re.search(fp, r.name) and r not in matches: matches.append(r) except re.error as e: print("ff regex compilation error", e) if len(matches) > 0: filtered_rs = matches if class_filters: matches = [] for r in filtered_rs: for cf in class_filters: try: if re.match(cf, r.__class__.__name__) and r not in matches: matches.append(r) except re.error as e: print("cf regex compilation error", e) filtered_rs = matches if previewing: matches = [] for r in filtered_rs: if not r.render_only: matches.append(r) filtered_rs = matches if len(viewer_solos) > 0: viewer_solos = [vs%len(filtered_rs) for vs in viewer_solos] solos = [] for i, r in enumerate(filtered_rs): if i in viewer_solos: solos.append(r) filtered_rs = solos return filtered_rs class SourceReader(): def __init__(self, filepath:Path=None, code:str=None, renderer=None, runner:str="default", inputs=None, cli_args=None, use_blender=False, ): self.filepath = None self.codepath = None self.data_out = None self.dirpath = None self.should_unlink = False self.program = None self.candidates = None self.renderer = renderer self.runner = runner self.inputs = inputs or [] self.use_blender = use_blender self.config = None self.read_configs(cli_args, filepath) if filepath or code: self.reset_filepath(filepath, code) def print_docstring(self): import ast tree = ast.parse(self.filepath.read_text()) docstring = ast.get_docstring(tree) if docstring: print("") print("📓 Doc:", docstring) print("") if docstring: return True @staticmethod def Script(name): root = Path(__file__).parent.parent if name.startswith("script:"): script = root / (name.replace(":", "s/") + ".py") if script.exists(): return script return False @staticmethod def Demos() -> dict[str,Path]: return {p.stem:p for p in (Path(__file__).parent.parent / "demo").glob("*.py")} @staticmethod def Tools() -> dict[str,Path]: return {p.stem:p for p in sorted((Path(__file__).parent.parent / "tools").glob("*.py"))} @staticmethod def Demo(name): root = Path(__file__).parent.parent demos = SourceReader.Demos() tools = SourceReader.Tools() if not name: return root / "demo/demo.py" elif name in demos.keys(): return demos[name] elif name in tools.keys(): return tools[name] else: return name @staticmethod def LoadDemo(demoname, **inputs): return SourceReader( SourceReader.Demo(demoname), inputs=inputs).renderables() @staticmethod def FrameResult(name, frame, inputs={}, renderer_state=None): filepath = SourceReader.Demo(name) sr = SourceReader(filepath, inputs=inputs) sr.unlink() return sr.frame_results(frame, renderer_state=renderer_state) def read_configs(self, args, filepath): embedded = Path(__file__).parent / ".coldtype.py" proj = Path(".coldtype.py") user = Path("~/.coldtype.py").expanduser() env = os.environ if on_windows(): _os = Path(".coldtype.win.py") elif on_mac(): _os = Path(".coldtype.mac.py") elif on_linux(): _os = Path(".coldtype.lin.py") files = [embedded, user, proj, _os] if args and hasattr(args, "config") and args.config: if args.config == "0": files = [] else: pass #if args.config == ".": # args.config = args.file #files.append(Path(args.config).expanduser()) if filepath: fp = Path(filepath).expanduser() if fp.exists() and not fp.is_dir(): if "# .coldtype" in fp.read_text(): files.append(fp) if args and args.file and args.config != "0": fp = Path(args.file).expanduser() if fp.exists() and not fp.is_dir(): if "# .coldtype" in fp.read_text(): files.append(fp) py_config = {} for p in files: if p.exists() and p.suffix == ".py": try: py_config = run_path(str(p), init_globals={ "__FILE__": p, "__inputs__": self.inputs, "__memory__": {}, "__as_config__": True, "__sibling__": partial(sibling, p), }) self.config = ColdtypeConfig(py_config, self.config if self.config else None, args) for f in self.config.font_dirs: if f not in ALL_FONT_DIRS: ALL_FONT_DIRS.insert(1, f) except Exception as e: if isinstance(e, ColdtypeCeaseConfigException): pass else: print("Failed to load config", p) print("Exception:", e) pass if len(files) == 0 or not self.config: self.config = ColdtypeConfig({}, None, args) # Simple overrides from environment variables for co in ConfigOption: if len(co.value) == 4: prop, _, _, cli_mod = co.value if prop.upper() in env: setattr(self.config, prop, cli_mod(env[prop.upper()])) if len(co.value) == 3: prop, _, _ = co.value if prop.upper() in env: setattr(self.config, prop, env[prop.upper()]) self.config.args = args #print(self.config.values()) def find_sources(self, dirpath, recursive=False): prefix = "**/" if recursive else "" globber = f"{prefix}*.py" if self.filepath: globber = f"{prefix}*" + self.filepath.suffix sources = list(dirpath.glob(globber)) #sources.extend(list(dirpath.glob("*.md"))) #print(">", sources) valid_sources = [] for p in sources: ignore = p.name.startswith("_") or p.name.startswith(".") if not ignore or p.name.startswith("__init"): valid_sources.append(p) valid_sources = sorted(valid_sources, key=lambda p: str(p.relative_to(dirpath))) valid_sources = sorted(valid_sources, key=lambda p: str(p.relative_to(dirpath)).count("/") > 0) #for p in valid_sources: # print(p.relative_to(dirpath)) return valid_sources def blender_io(self): #if not self.use_blender and not self.config.blender_watch: # return None bf = self.config.blender_file if not bf: bf = self.filepath.parent / "blends" / (self.filepath.stem + ".blend") return BlenderIO(bf) def normalize_filepath(self, filepath:Path, dirindex=0): if isinstance(filepath, str): filepath = Path(filepath) filepath = filepath.expanduser().resolve() if filepath.is_dir(): self.dirpath = filepath #filepath = sorted(list(filepath.glob("*.py")), key=lambda p: p.stem)[dirindex] filepath = self.find_sources(self.dirpath)[dirindex] else: if self.dirpath is None: self.dirpath = filepath.parent if not filepath.exists(): with_py = (filepath.parent / (filepath.stem + ".py")) if with_py.exists(): filepath = with_py if filepath.suffix not in [".md", ".rst", ".py"]: raise Exception("Coldtype can only read .py, .md, and .rst files") return filepath def adjacents(self): if self.dirpath: return self.find_sources(self.dirpath) def reset_filepath(self, filepath:Path, code:str=None, reload:bool=True, dirdirection=0): self.unlink() self.should_unlink = False dirindex = 0 if self.dirpath and dirdirection != 0: # find index of existing filepath, increment by dirdirection? pys = self.find_sources(self.dirpath) #pys = sorted(list(self.dirpath.glob("*.py")), key=lambda p: p.stem) curr = pys.index(self.filepath) adj = (curr + dirdirection) % len(pys) #print(curr, adj) filepath = pys[adj] if filepath: self.filepath = self.normalize_filepath(filepath, dirindex) if not self.filepath.exists(): raise Exception("Source file does not exist") else: if code: self.write_code_to_tmpfile(code) else: raise Exception("Must provide filepath or code") if reload: self.reload() return self.filepath def find_versions(self, initial, restart_count): source_code = self.codepath.read_text() versions = None versions_file = self.filepath.parent / (self.filepath.stem + "_versions.py") if versions_file.exists(): sr = SourceReader(versions_file) versions = sr.program["VERSIONS"] if re.findall(r"VERSIONS\s?=", source_code): try: versions = re.findall(r"VERSIONS\s?=.*\#\/VERSIONS", source_code, re.DOTALL)[0] except IndexError: return None, None versions = eval(re.sub(r"VERSIONS\s?=", "", versions)) if not isinstance(versions, dict): if not isinstance(versions[0], dict): versions = {v:dict(idx=idx) for idx, v in enumerate(versions)} else: versions = {str(idx):v for idx, v in enumerate(versions)} if versions: versions = {k:{**v, **dict(key=k)} for k,v in versions.items()} versions = list(versions.values()) vi = self.renderer.source_reader.config.version_index if initial and restart_count == 0: if len(self.inputs) > 0: for vidx, v in enumerate(versions): if self.inputs[0] == v["key"]: vi = vidx if vi is None: vi = 0 self.renderer.source_reader.config.version_index = vi version = versions[vi] self.renderer.state.versions = versions source_code = source_code.replace("ƒVERSION", str(version["key"])) self.codepath.write_text(source_code) return version, versions return None, None def reload(self, code:str=None, output_folder_override=None, initial=False, restart_count=0, ui={}, ): if code: self.write_code_to_tmpfile(code) self.codepath, self.data_out = read_source_to_tempfile(self.filepath, self.codepath, renderer=self.renderer, source_reader=self) memory = {} if self.renderer: memory = self.renderer.state.memory version, versions = self.find_versions(initial, restart_count) blendering = bool(self.use_blender or self.config.blender_watch) self.program = run_source( self.filepath, self.codepath, self.inputs, memory, self.config, __RUNNER__=self.runner, __BLENDER__=self.blender_io(), __BLENDERING__=blendering, __VERSION__=version, __VERSIONS__=versions, ui=ui) self.candidates = self.renderable_candidates( output_folder_override, self.config.add_time_viewers, self.config.add_ui) def write_code_to_tmpfile(self, code): if self.filepath: self.filepath.unlink() with NamedTemporaryFile("w", prefix="coldtype_", suffix=".py", delete=False) as tf: tf.write(code) self.filepath = Path(tf.name) self.should_unlink = True def renderable_candidates(self, output_folder_override=None, add_time_viewers=False, add_ui=False, ): candidates = find_renderables( self.filepath, self.codepath, self.program, output_folder_override, blender_io=self.blender_io(), args=self.renderer.args if self.renderer else None) if len(candidates) == 0: candidates.append(Programs.Blank()) widest = None if add_ui: for c in candidates: if not widest and hasattr(c, "rect"): widest = c elif hasattr(c, "rect") and c.rect.w > widest.rect.w: widest = c if widest: candidates.insert(0, uiView(widest)) if add_time_viewers: out = [] for c in candidates: if (isinstance(c, animation) and not isinstance(c, aframe) and not isinstance(c, ui) ): out.extend(timeViewer(c)) out.append(c) return out else: return candidates def renderables(self, viewer_solos=[], function_filters=[], class_filters=[], previewing=False, ): if not function_filters and self.config.function_filters: function_filters = self.config.function_filters return filter_renderables(self.candidates, viewer_solos=viewer_solos, function_filters=function_filters, class_filters=class_filters, previewing=previewing) def frame_results(self, frame, class_filters=[], renderer_state=None): rs = self.renderables(class_filters=class_filters) res = [] for r in rs: if isinstance(r, runnable): res.append([r, None]) continue ps = r.passes(None, renderer_state, indices=[frame]) for p in ps: res.append([r, r.run_normal(p, renderer_state=renderer_state)]) return res def unlink(self): if self.should_unlink and self.filepath: if self.filepath.exists(): self.filepath.unlink() if self.codepath: if self.codepath.exists(): self.codepath.unlink() return self class Programs(): @staticmethod def Demo(): return SourceReader.LoadDemo("demo")[0] @staticmethod def Blank(): return SourceReader.LoadDemo("blank")[0] ================================================ FILE: packages/coldtype-core/src/coldtype/renderer/state.py ================================================ import json, inspect from pathlib import Path from coldtype.geometry import Point from coldtype.renderable import Action class RendererStateEncoder(json.JSONEncoder): def default(self, o): return { "controller_values": o.controller_values } class RendererState(): def __init__(self, renderer): self.renderer = renderer self.previewing = False self.preview_scale = 1 self.controller_values = {} self.overlays = {} self.frame_offset = 0 self.canvas = None self._last_filepath = None self.cv2caps = {} self.inputs = [] self.memory = None self.memory_initial = None self.versions = [] self.playing = False self.mouse_down = False self.cursor = Point(0, 0) self.cursor_history = [] for c in renderer.args.last_cursor.split(";"): cp = Point([float(p) for p in c.split(",")]) self.cursor_history.append(cp) if len(self.cursor_history) > 0: self.cursor = self.cursor_history[-1] self.reset() def reset(self, ignore_current_state=False): if self.filepath == self._last_filepath and not ignore_current_state: return if self.filepath: self._last_filepath = self.filepath try: deserial = json.loads(self.filepath.read_text()) cv = deserial.get("controller_values") if cv: self.controller_values = cv except json.decoder.JSONDecodeError: self.controller_values = {} except FileNotFoundError: self.controller_values = {} def clear(self): if self.filepath: self.filepath.write_text("") self.reset() @property def filepath(self): if self.renderer and self.renderer.source_reader.filepath: return Path(str(self.renderer.source_reader.filepath).replace(".py", "") + "_state.json") else: return None @property def midi(self): return self.controller_values def persist(self): if self.filepath: print("Saving Controller State...") self.filepath.write_text(RendererStateEncoder().encode(self)) else: print("No source; cannot persist state") def record_cursor(self, pos): self.cursor = pos.scale(1/self.preview_scale).round_to(1) return self.cursor #return Action.PreviewStoryboard def on_mouse_button(self, pos, btn, action, mods): self.mouse_down = action if action == 0: if on_click := self.renderer.source_reader.program.get("on_click"): arg_count = len(inspect.signature(on_click).parameters) args = [pos, btn, mods][:arg_count] on_click(*args) if not self.playing and action == 0: for r in self.renderer.renderables(None): if not hasattr(r, "_stacked_rect"): continue sr = r._stacked_rect.flip(self.renderer.extent.h) if Point(*pos).inside(sr): if hasattr(r, "pointToFrame"): fo = r.pointToFrame(Point(*pos)) self.frame_offset = fo return Action.PreviewStoryboard p = self.record_cursor(pos) #if self.cursor_history[-1] != p: self.cursor_history.append(p) return Action.PreviewStoryboard def on_mouse_move(self, pos): if self.mouse_down: for r in self.renderer.renderables(None): sr = r._stacked_rect.flip(self.renderer.extent.h) if Point(*pos).inside(sr): if hasattr(r, "pointToFrame"): fo = r.pointToFrame(Point(*pos)) self.frame_offset = fo return Action.PreviewStoryboard if self.playing: self.record_cursor(pos) def mod_preview_scale(self, inc, absolute=0): if absolute > 0: ps = absolute else: ps = self.preview_scale + inc self.preview_scale = max(0.1, min(5, ps)) return Action.PreviewStoryboardReload def toggle_overlay(self, overlay, force=None): if force is not None: v = force else: v = not self.overlays.get(overlay, False) if not v: if overlay in self.overlays: del self.overlays[overlay] else: self.overlays[overlay] = True ================================================ FILE: packages/coldtype-core/src/coldtype/renderer/ui.py ================================================ from collections import defaultdict from coldtype.renderable import renderable, ui, animation from coldtype.text.composer import StSt, Font, Rect, Point, Style from coldtype.timing.timeline import Timeline from coldtype.timing.midi import MidiTimeline from coldtype.runon.path import P from coldtype.color import bw, hsl from coldtype.renderer.keyboard import KeyboardShortcut, SHORTCUTS from functools import partial def uiView(_renderable): r = Rect(max(1080, _renderable.rect.w), 80) shortcuts = [[ KeyboardShortcut.ShowInFinder, KeyboardShortcut.RenderAll, KeyboardShortcut.OpenInEditor, KeyboardShortcut.Quit, ]] if isinstance(_renderable, animation): r = Rect(r.w, r.h+80) shortcuts.append([ KeyboardShortcut.PlayPreview, KeyboardShortcut.Release, KeyboardShortcut.ToggleTimeViewer, KeyboardShortcut.ToggleUI, ]) @renderable(r, bg=0, preview_only=1) def _uiView(r): rows = r.subdivide_with_leading(len(shortcuts), 2, "N") def btn(ri, x): ks:KeyboardShortcut = shortcuts[ri][x.i] shortcut = SHORTCUTS[ks][0] mods = "+".join(shortcut[0]) if mods: keys = mods + "+" + shortcut[1] else: keys = shortcut[1] return (P( StSt(ks.name, Font.JBMono(), 24, wght=0.25).f(1), StSt(keys, Font.JBMono(), 24, wght=1).f(1) ) .stack(12) .align(x.el.inset(14), "W") .insert(0, P(x.el).f(0.2))) def row(x): rs = x.el.subdivide_with_leading(4, 2, "W") return P().enumerate(rs, partial(btn, x.i)) return P().enumerate(rows, row) return _uiView ================================================ FILE: packages/coldtype-core/src/coldtype/renderer/utils.py ================================================ import ast, hashlib from enum import Enum from coldtype.osutil import on_linux, on_mac, on_windows, System, play_sound class Watchable(Enum): Source = "Source" Font = "Font" Library = "Library" Generic = "Generic" class bcolors: HEADER = '\033[95m' OKBLUE = '\033[94m' OKGREEN = '\033[92m' WARNING = '\033[93m' FAIL = '\033[91m' ENDC = '\033[0m' BOLD = '\033[1m' UNDERLINE = '\033[4m' def bc_print(which, *txt): print(f"{which}{' '.join(txt)}{bcolors.ENDC}") def bytesto(bytes): r = float(bytes) for i in range(2): r = r / 1024 return(r) def chunks(lst, n): """Yield successive n-sized chunks from lst.""" for i in range(0, len(lst), n): yield lst[i:i + n] def file_and_line_to_def(filepath, lineno): # https://julien.danjou.info/finding-definitions-from-a-source-file-and-a-line-number-in-python/ candidate = None for item in ast.walk(ast.parse(filepath.read_text())): if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): if item.lineno > lineno: continue if candidate: distance = lineno - item.lineno if distance < (lineno - candidate.lineno): candidate = item else: candidate = item if candidate: return candidate.name def path_hash(path): return hashlib.sha1(str(path.resolve()).encode("UTF-8")).hexdigest()[:10] ================================================ FILE: packages/coldtype-core/src/coldtype/renderer/winman/__init__.py ================================================ import time, threading, sys from coldtype.renderer.config import ColdtypeConfig from coldtype.renderer.winman.passthrough import WinmanPassthrough from coldtype.renderer.winman.glfwskia import glfw, skia, WinmanGLFWSkia, WinmanGLFWSkiaBackground from coldtype.renderer.winman.audio import WinmanAudio from coldtype.renderer.winman.midi import MIDIWatcher, rtmidi from coldtype.renderer.winman.blender import WinmanBlender from coldtype.renderable import Action, Overlay last_line = '' new_line_event = threading.Event() def monitor_stdin(): # https://stackoverflow.com/questions/27174736/how-to-read-most-recent-line-from-stdin-in-python global last_line global new_line_event def keep_last_line(): global last_line, new_line_event for line in sys.stdin: last_line = line new_line_event.set() keep_last_line_thread = threading.Thread(target=keep_last_line) keep_last_line_thread.daemon = True keep_last_line_thread.start() class Winmans(): def __init__(self, renderer, config:ColdtypeConfig): self.renderer = renderer self.config = config self.pt = WinmanPassthrough() self.glsk:WinmanGLFWSkia = None self.midi:MIDIWatcher = None self.b3d:WinmanBlender = None self.audio:WinmanAudio = None self.last_title = "coldtype" self.last_time = -1 self.refresh_delay = self.config.refresh_delay self.backoff_refresh_delay = self.refresh_delay self.title_states = { "playing": False, "rendered": False, "audio": False, } self.print_approx_fps = False self.bg = False if (config.args.is_subprocess or config.args.all or config.args.release or config.args.build or config.no_watch ): self.bg = True self.cron_start = 0 self.cron_interval = 0 def should_glfwskia(self): return glfw is not None and skia is not None and not self.config.no_viewer def should_midi(self): return rtmidi and not self.config.no_midi def should_audio(self): return WinmanAudio.Possible() def should_blender(self): return self.config.blender_watch def add_viewers(self): if not self.bg: monitor_stdin() if self.should_glfwskia(): self.glsk = WinmanGLFWSkia(self.config, self.renderer) if self.should_blender(): self.b3d = WinmanBlender(self.config) if self.should_midi(): self.midi = MIDIWatcher(self.config, self.renderer.state, self.renderer.execute_string_as_shortcut_or_action) elif self.config.args.midi_info: print(">>> pip install rtmidi") if self.should_audio(): self.audio = WinmanAudio() def did_reset_extent(self, extent): if self.glsk: self.glsk.reset_extent(extent) def did_reload(self, filepath, source_reader): if self.b3d: self.b3d.reload(filepath, source_reader) def did_reload_animation(self, rs): if self.audio: self.audio.reload_with_animation(rs) def did_render(self, count, ditto_last, renders): if self.b3d: self.b3d.did_render(count, ditto_last, renders) def found_blend_files(self, blend_files): if len(blend_files) > 0: if self.b3d: self.b3d.launch(blend_files[0]) def all(self): return [self.pt, self.glsk, self.b3d] def map(self): for wm in self.all(): if wm: yield wm def set_title(self, text): self.last_title = text [wm.set_title(text) for wm in self.map()] def mod_title(self, state, value): self.title_states[state] = value ts = self.last_title for k, v in self.title_states.items(): if v: ts = ts + " / " + k [wm.set_title(ts) for wm in self.map()] def reset(self): [wm.reset() for wm in self.map()] self.toggle_rendered(force=False) def terminate(self): [wm.terminate() for wm in self.map()] def toggle_rendered(self, force=None): self.renderer.state.toggle_overlay(Overlay.Rendered, force=force) if Overlay.Rendered in self.renderer.state.overlays: self.mod_title("rendered", True) else: self.mod_title("rendered", False) def toggle_playback(self): if self.renderer.state.playing == 0: self.renderer.state.playing = 1 self.mod_title("playing", True) else: self.renderer.state.playing = 0 self.mod_title("playing", False) if not self.glsk: if self.b3d: self.b3d.toggle_playback(self.renderer.state.playing) def frame_offset(self, offset): self.renderer.state.frame_offset += offset # if not self.glsk: # if self.b3d: # self.b3d.frame_offset(offset) def should_close(self): return any([wm.should_close() for wm in self.map()]) def send_to_external(self, action, **kwargs): pass def poll(self): if self.glsk: self.glsk.poll() def turn_over(self): if self.b3d and self.b3d.subp: if self.b3d.subp.poll() == 0: self.renderer.dead = True self.renderer.on_exit() return if self.midi: tripped = self.midi.monitor(self.renderer.state.playing) if tripped: self.renderer.action_waiting = Action.PreviewStoryboard self.renderer.action_waiting_reason = "midi_trigger" render_previews = len(self.renderer.previews_waiting) > 0 if not render_previews: self.backoff_refresh_delay += 0.01 if self.backoff_refresh_delay >= 0.25: self.backoff_refresh_delay = 0.25 return [] self.backoff_refresh_delay = self.refresh_delay did_preview = [] if self.glsk: did_preview.append(self.glsk.turn_over()) if len(did_preview) > 0: la = self.renderer.last_animation if la: fo = abs(self.renderer.state.frame_offset%la.duration) if self.config.enable_audio and self.audio: self.audio.play_frame(fo) if fo == la.duration-1: if self.renderer.stop_at_end: self.renderer.stop_at_end = False self.renderer.action_waiting = Action.PreviewPlay self.renderer.action_waiting_reason = "stopping_at_end" print("END!") return did_preview def run_loop(self): while (not self.renderer.dead and not self.should_close() ): t2 = time.time() td2 = t2 - self.last_time spf = 0.1 if self.renderer.last_animation: spf = 1 / (float(self.renderer.last_animation.timeline.fps)*self.renderer.viewer_playback_rate) if td2 >= spf: if self.print_approx_fps: print("APPROX FPS ==", 1/td2 * self.renderer.viewer_sample_frames) self.print_approx_fps = False self.last_time = t2 else: self.poll() continue if self.renderer.state.playing: time.sleep(0.01) else: time.sleep(self.backoff_refresh_delay) self.last_time = t2 # TODO the main turn_over, why is it like this? self.last_previews = self.renderer.turn_over() global last_line if last_line: lls = last_line.strip() if lls: self.renderer.on_stdin(lls) last_line = None if self.cron_start > 0 and self.cron_interval > 0: if (time.time() - self.cron_start) > self.cron_interval: self.cron_start = time.time() self.renderer.action_waiting = Action.PreviewStoryboardReload self.renderer.action_waiting_reason = "cron_interval timer hit" self.poll() self.renderer.on_exit(restart=False) ================================================ FILE: packages/coldtype-core/src/coldtype/renderer/winman/audio.py ================================================ from coldtype.renderer.winman.passthrough import WinmanPassthrough from coldtype.renderable.animation import animation import wave try: import pyaudio import soundfile import numpy as np from soundfile import SoundFile except ImportError: pyaudio = None soundfile = None np = None class WinmanAudio(WinmanPassthrough): @staticmethod def Possible(): return bool(pyaudio and soundfile and np) def __init__(self): self.pa = pyaudio.PyAudio() self.pa_src:SoundFile = None self.pa_stream = None self.pa_rate = 0 self.a = None def recycle(self): if self.pa_stream: self.pa_stream.stop_stream() self.pa_stream.close() if self.pa_src: self.pa_src.close() self.pa_stream = None self.pa_src = None def reload_with_animation(self, a:animation): self.recycle() self.a = a if a.audio: try: self.pa_src = soundfile.SoundFile(a.audio, "r") except: print(">>> Could not load audio file (corrupted?)") self.pa_src = None def play_frame(self, frame): #if not self.args.preview_audio: # return if pyaudio and self.pa_src: hz = self.pa_src.samplerate width = self.pa_src.channels if not self.pa_stream or hz != self.pa_rate: self.pa_rate = hz self.pa_stream = self.pa.open( format=pyaudio.paFloat32, channels=width, rate=hz, output=True) audio_frame = frame chunk = int(hz / self.a.timeline.fps) try: self.pa_src.seek(chunk*audio_frame) data = self.pa_src.read(chunk) try: data = data.astype(np.float32).tostring() except AttributeError: # https://github.com/coldtype/coldtype/discussions/185#discussioncomment-16507693 data = data.astype(np.float32).tobytes() self.pa_stream.write(data) except wave.Error: print(">>> Could not read audio at frame", audio_frame) def play_once(self, animation): if not animation.audio: return wf = wave.open(str(animation.audio), 'rb') stream = self.p.open( format=self.p.get_format_from_width( wf.getsampwidth()), channels = wf.getnchannels(), rate = wf.getframerate(), output = True) chunk = 1024 data = wf.readframes(chunk) # Play the sound by writing the audio data to the stream while data != '': stream.write(data) data = wf.readframes(chunk) stream.close() print("PLAY!", animation.audio) pass def terminate(self): self.recycle() self.pa.terminate() ================================================ FILE: packages/coldtype-core/src/coldtype/renderer/winman/blender.py ================================================ from pathlib import Path from coldtype.renderer.utils import path_hash from coldtype.renderer.winman.passthrough import WinmanPassthrough from coldtype.blender.render import blender_launch_livecode from coldtype.blender import BlenderIO class WinmanBlender(WinmanPassthrough): def __init__(self, config): self.subp = None self.command_file = None try: from b3denv import get_vars b3d_vars = get_vars(None) self.blender_path = Path(b3d_vars["blender"]) except: raise Exception("NO BLENDER FOUND (via b3denv)") self.reset_factory = config.blender_reset_factory self.cli_args = config.blender_command_line_args def launch(self, blender_io:BlenderIO): if self.subp: self.subp.kill() self.subp = blender_launch_livecode( self.blender_path, blender_io.blend_file, self.command_file, #reset_factory=self.reset_factory, additional_args=self.cli_args) def write_command(self, cmd, arg, kwargs=[]): try: cb = self.command_file if cb.exists(): cb.unlink() cb.write_text(f"{cmd},{str(arg)};{str(kwargs)}") except FileNotFoundError: pass def did_render(self, count, ditto_last, renders): if ditto_last: self.write_command("refresh_sequencer_and_image", count) else: self.write_command("refresh_sequencer", count) def reload(self, filepath, source_reader): ph = path_hash(filepath) self.command_file = Path(f"~/.coldtype/{ph}.txt").expanduser() self.write_command("import", filepath, source_reader.inputs) def toggle_playback(self, toggle): self.write_command("play_preview", toggle) def frame_offset(self, offset): self.write_command("frame_offset", offset) def terminate(self): if self.subp: self.subp.kill() ================================================ FILE: packages/coldtype-core/src/coldtype/renderer/winman/glfwskia.py ================================================ from coldtype.renderable.renderable import Overlay from coldtype.renderer.winman.passthrough import WinmanPassthrough import time as ptime import coldtype.skiashim as skiashim from subprocess import Popen, PIPE from pathlib import Path from coldtype.color import rgb, hsl from coldtype.osutil import on_mac try: import glfw except ImportError: #print("Big problem: GLFW could not be loaded") glfw = None try: import skia import coldtype.fx.skia as skfx except ImportError: skia = None skfx = None try: from OpenGL import GL except ImportError: try: from OpenGL import GL except: GL = None from coldtype.geometry import Point, Rect, Edge from coldtype.renderable import Action from coldtype.renderer.config import ConfigOption, ColdtypeConfig from coldtype.renderer.keyboard import KeyboardShortcut, REPEATABLE_SHORTCUTS, shortcuts_keyed, LAYOUT_REMAPS try: from coldtype.pens.svgpen import SVGPen except ModuleNotFoundError: SVGPen = None def glfw_generic_setup(): glfw.window_hint(glfw.STENCIL_BITS, 8) # https://www.glfw.org/faq#macos if on_mac(): glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3) glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 2) glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, True) glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE) class WinmanGLFWSkiaBackground(WinmanPassthrough): def __init__(self, config:ColdtypeConfig, renderer): # TODO is the glfw stuff here actually necessary? if not glfw.init(): raise RuntimeError('glfw.init() failed') glfw.window_hint(glfw.VISIBLE, glfw.FALSE) glfw_generic_setup() window = glfw.create_window(640, 480, '', None, None) glfw.make_context_current(window) self.context = skia.GrDirectContext.MakeGL() def reset_extent(self, extent): pass class WinmanGLFWSkia(): def __init__(self, config:ColdtypeConfig, renderer, background=False): self.config = config self.window = None self.background = background self.renderer = renderer self.primary_monitor = None self.primary_monitor_rect = None self.all_shortcuts = shortcuts_keyed() self.prev_scale = 0 self.surface = None if self.renderer.args.print_skia_version: print(f"skia version: {skia.__version__} (m87={skiashim.SKIA_87})") parent = Path(__file__).parent self.typeface = skia.Typeface.MakeFromFile(str(parent / "../../demo/RecMono-CasualItalic.ttf")) self.transparency_blocks = skia.Image.MakeFromEncoded(skia.Data.MakeFromFileName(str(parent / "../../demo/transparency_blocks.png"))) if not glfw.init(): raise RuntimeError('glfw.init() failed') glfw_generic_setup() glfw.window_hint(glfw.RESIZABLE, glfw.FALSE) if self.config.window_chromeless: glfw.window_hint(glfw.DECORATED, glfw.FALSE) else: glfw.window_hint(glfw.DECORATED, glfw.TRUE) if self.config.window_transparent: glfw.window_hint(glfw.TRANSPARENT_FRAMEBUFFER, glfw.TRUE) else: glfw.window_hint(glfw.TRANSPARENT_FRAMEBUFFER, glfw.FALSE) if self.config.window_passthrough: try: glfw.window_hint(0x0002000D, glfw.TRUE) # glfw.window_hint(glfw.MOUSE_PASSTHROUGH, glfw.TRUE) except glfw.GLFWError: print("failed to hint window for mouse-passthrough") if self.config.window_background: glfw.window_hint(glfw.FOCUSED, glfw.FALSE) if self.config.window_float: glfw.window_hint(glfw.FLOATING, glfw.TRUE) if not self.background: self.window = glfw.create_window(int(10), int(10), '', None, None) self.window_scrolly = 0 self.window_focus = 0 self.find_primary_monitor() glfw.make_context_current(self.window) glfw.set_key_callback(self.window, self.on_key) #glfw.set_char_callback(self.window, self.on_char) glfw.set_mouse_button_callback(self.window, self.on_mouse_button) glfw.set_cursor_pos_callback(self.window, self.on_mouse_move) glfw.set_scroll_callback(self.window, self.on_scroll) glfw.set_window_focus_callback(self.window, self.on_focus) glfw.set_framebuffer_size_callback(self.window, self.on_resize) self.set_window_opacity() self.prev_scale = self.get_content_scale() self.context = skia.GrDirectContext.MakeGL() self.copy_previews_to_clipboard = False def find_primary_monitor(self): self.primary_monitor = glfw.get_primary_monitor() found_primary = None if self.config.monitor_name: remn = self.config.monitor_name monitors = glfw.get_monitors() matches = [] if remn == "list": print("> MONITORS") for monitor in monitors: mn = glfw.get_monitor_name(monitor) if remn == "list": print(" -", mn.decode("utf-8")) elif remn in str(mn): matches.append(monitor) if len(matches) > 0: found_primary = matches[0] if found_primary: self.primary_monitor = found_primary vm = glfw.get_video_mode(self.primary_monitor) work_rect = Rect(vm.size.width, vm.size.height) self.primary_monitor_rect = work_rect def create_surface(self, rect): mode = GL.GL_RGBA8 #mode = GL.GL_COMPRESSED_RGB8_ETC2 backend_render_target = skia.GrBackendRenderTarget( int(rect.w), int(rect.h), 0, 0, skia.GrGLFramebufferInfo(0, mode)) self.surface = skia.Surface.MakeFromBackendRenderTarget( self.context, backend_render_target, skia.kBottomLeft_GrSurfaceOrigin, skia.kRGBA_8888_ColorType, skia.ColorSpace.MakeSRGB()) assert self.surface is not None def get_content_scale(self): u_scale = self.config.window_content_scale if u_scale: return u_scale elif glfw and not self.renderer.args.no_viewer: if self.primary_monitor: return glfw.get_monitor_content_scale(self.primary_monitor)[0] else: return glfw.get_window_content_scale(self.window)[0] else: return 1 def content_scale_changed(self): scale_x = self.get_content_scale() if scale_x != self.prev_scale: self.prev_scale = scale_x return True return False def update_window(self, frect): m_scale = self.get_content_scale() scale_x, scale_y = m_scale, m_scale ww = int(frect.w/scale_x) wh = int(frect.h/scale_y) glfw.set_window_size(self.window, ww, wh) pin = self.config.window_pin vm = glfw.get_video_mode(self.primary_monitor) work_rect = Rect(vm.size.width, vm.size.height) self.primary_monitor_rect = work_rect if pin and pin != "0": _work_rect_x, _work_rect_y = Rect(glfw.get_monitor_workarea(self.primary_monitor)).xy() if _work_rect_y < 100: _work_rect_y = 0 else: _work_rect_y -= 38 if _work_rect_x < 100: _work_rect_x = 0 #print(_work_rect_x, _work_rect_y) wrz = work_rect.zero() edges = Edge.PairFromCompass(pin) pinned = wrz.take(ww, edges[0]).take(wh, edges[1]).round() #if edges[1] == "mdy": # pinned = pinned.offset(0, -30) pinned = pinned.flip(wrz.h) #pinned.drop(_work_rect_x, "E") #pinned.drop(_work_rect_y, "S") #wpi = self.config.window_pin_inset #pinned = pinned.inset(-wpi[0], wpi[1]) wpox = self.config.window_pin_offset_x wpoy = self.config.window_pin_offset_y pinned = pinned.offset(wpox, -wpoy) glfw.set_window_pos(self.window, pinned.x + _work_rect_x, pinned.y + _work_rect_y) else: glfw.set_window_pos(self.window, 0, 0) def set_title(self, text): glfw.set_window_title(self.window, text) def set_window_opacity(self, relative=None, absolute=None): if relative is not None: o = glfw.get_window_opacity(self.window) op = o + float(relative) elif absolute is not None: op = float(absolute) else: op = float(self.config.window_opacity) glfw.set_window_opacity(self.window, max(0.1, min(1, op))) def reset(self): self.window_scrolly = 0 def should_close(self): return glfw.window_should_close(self.window) def focus(self, force=False): if self.window_focus == 0 or force: glfw.focus_window(self.window) return True return False def allow_mouse(self): return True def on_scroll(self, win, xoff, yoff): self.window_scrolly += yoff #self.on_action(Action.PreviewStoryboard) #print(xoff, yoff) #pass # TODO! def on_focus(self, win, focus): self.window_focus = focus def on_resize(self, win, w, h): #self.renderer.action_waiting = Action.PreviewStoryboard #self.renderer.action_waiting_reason = "on_resize" pass def on_mouse_button(self, _, btn, action, mods): if not self.allow_mouse(): return pos = Point(glfw.get_cursor_pos(self.window)).scale(2) # TODO should this be preview-scale? pos[1] = self.renderer.extent.h - pos[1] try: requested_action = self.renderer.state.on_mouse_button(pos, btn, action, mods) if requested_action: self.renderer.action_waiting = requested_action self.renderer.action_waiting_reason = "mouse_trigger" except Exception as e: print(e) def on_mouse_move(self, _, xpos, ypos): if not self.allow_mouse(): return pos = Point((xpos, ypos)).scale(2) pos[1] = self.renderer.extent.h - pos[1] requested_action = self.renderer.state.on_mouse_move(pos) if requested_action: self.renderer.action_waiting = requested_action self.renderer.action_waiting_reason = "mouse_move" def on_key(self, win, key, scan, action, mods): self.on_potential_shortcut(key, action, mods) # if key in GLFW_SPECIALS_LOOKUP.values(): # self.on_potential_shortcut(key, action, mods) # elif mods: # self.on_potential_shortcut(key, action, mods) def on_char(self, win, key): ck = chr(key) print("CHAR", ck) self.on_potential_shortcut(ck, glfw.PRESS, []) def repeatable_shortcuts(self): return REPEATABLE_SHORTCUTS def on_potential_shortcut(self, key, action, mods): if self.config.keyboard_layout is not None and self.config.keyboard_layout in LAYOUT_REMAPS: remap = LAYOUT_REMAPS[self.config.keyboard_layout] if key in remap: key = remap[key] for shortcut, options in self.all_shortcuts.items(): for modifiers, skey, ckey in options: #print(key, skey, ckey) if key != skey and key != ckey: continue if isinstance(mods, list): mod_matches = mods else: mod_matches = [0, 0, 0, 0] for idx, mod in enumerate([glfw.MOD_SUPER, glfw.MOD_ALT, glfw.MOD_SHIFT, glfw.MOD_CONTROL]): if mod in modifiers: if mods & mod: mod_matches[idx] = 1 elif mod not in modifiers: if not (mods & mod): mod_matches[idx] = 1 mod_match = all(mod_matches) if not mod_match and len(modifiers) == 0 and isinstance(mods, list): mod_match = True if len(modifiers) == 0 and mods != 0 and not isinstance(mods, list): mod_match = False if mod_match and (key == skey or key == ckey): if (action == glfw.REPEAT and shortcut in self.repeatable_shortcuts()) or action == glfw.PRESS: #print(shortcut, modifiers, skey, mod_match) return self.renderer.on_shortcut(shortcut) def reset_extent(self, extent): GL.glClear(GL.GL_COLOR_BUFFER_BIT) self.create_surface(extent) self.update_window(extent) self.surface.flushAndSubmit() glfw.swap_buffers(self.window) def turn_over(self): extent = self.renderer.extent if self.renderer.needs_new_context: self.renderer.needs_new_context = False self.reset_extent(extent) #self.update_window(extent) #if not self.surface: #self.create_surface(extent) GL.glClear(GL.GL_COLOR_BUFFER_BIT) did_preview = [] with self.surface as canvas: canvas.clear(skia.Color4f(0.3, 0.3, 0.3, 1)) if self.config.window_transparent: canvas.clear(skia.Color4f(0.3, 0.3, 0.3, 0)) for idx, (render, result, rp) in enumerate(self.renderer.previews_waiting): sr = render._stacked_rect rect = sr.offset((extent.w-sr.w)/2, 0).round() if self.copy_previews_to_clipboard: try: svg = SVGPen.Composite(result, render.rect, viewBox=render.viewBox) print(svg) process = Popen( 'pbcopy', env={'LANG': 'en_US.UTF-8'}, stdin=PIPE) process.communicate(svg.encode('utf-8')) except Exception as e: print(">>>>>>>>>>>", e) print("failed to copy to clipboard") try: preview_scale = render.preview_scale * self.renderer.state.preview_scale self.draw_preview(idx, preview_scale, canvas, rect, (render, result, rp)) did_preview.append(rp) except Exception as e: short_error = self.renderer.print_error() paint = skia.Paint(AntiAlias=True, Color=skia.ColorRED) canvas.drawString(short_error, 10, 32, skia.Font(None, 36), paint) self.copy_previews_to_clipboard = False self.surface.flushAndSubmit() glfw.swap_buffers(self.window) return did_preview def poll(self): glfw.poll_events() def draw_preview(self, idx, scale, canvas, rect, waiter): render, result, rp = waiter if isinstance(waiter[1], Path) or isinstance(waiter[1], str): image = skia.Image.MakeFromEncoded(skia.Data.MakeFromFileName(str(waiter[1]))) if image: canvas.save() canvas.scale(scale, scale) skiashim.canvas_drawImage(canvas, image, rect.x, rect.y) canvas.restore() return error_color = rgb(1, 1, 1).skia() canvas.save() canvas.translate(0, self.window_scrolly) canvas.translate(rect.x, rect.y) if not self.config.window_transparent: if not render.layer: matrix = skia.Matrix() canvas.drawRect(skia.Rect(0, 0, rect.w, rect.h), { "Shader": skiashim.image_makeShader(self.transparency_blocks, matrix), }) #canvas.drawRect(skia.Rect(0, 0, rect.w, rect.h), skia.Paint(Color=hsl(0.3).skia())) if not hasattr(render, "show_error"): canvas.scale(scale, scale) if render.clip: canvas.clipRect(skia.Rect(0, 0, rect.w, rect.h)) if render.direct_draw: try: render.run(rp, self.renderer.state, canvas) except Exception as e: short_error = self.renderer.print_error() render.show_error = short_error error_color = rgb(0, 0, 0).skia() else: do_precompose = True #do_precompose = self.renderer.args.never_reuse_skia_context or render.single_frame or render.composites and not render.interactable or self.config.preview_saturation != 1 if do_precompose: from coldtype.img.skiaimage import SkiaImage postprocess = render.postprocessor(result) comp = render.precompose(result, scale) comp_img = comp.img().get("src") if not self.renderer.last_render_cleared: render.last_result = SkiaImage(comp_img) else: render.last_result = None canvas.save() canvas.scale(1/scale, 1/scale) if postprocess: comp = render.precompose(postprocess(comp), scale) comp_img = comp.img().get("src") paint = skia.Paint() paint.setColorFilter(skfx.Skfi.saturate(self.config.preview_saturation)) skiashim.canvas_drawImage(canvas, comp_img, 0, 0, paint) canvas.restore() else: from coldtype.pens.skiapen import SkiaPen sr = render.rect.scale(scale, "mnx", "mxx") SkiaPen.CompositeToCanvas(result, sr, canvas, scale, style=render.style) #comp = result #render.draw_preview(1.0, canvas, render.rect, comp, rp) if hasattr(render, "show_error"): paint = skia.Paint(AntiAlias=True, Color=error_color) canvas.drawString(render.show_error, 30, 70, skia.Font(self.typeface, 50), paint) canvas.drawString("> See process in terminal for traceback", 30, 120, skia.Font(self.typeface, 32), paint) canvas.restore() def terminate(self): glfw.terminate() if self.context: self.context.abandonContext() ================================================ FILE: packages/coldtype-core/src/coldtype/renderer/winman/midi.py ================================================ try: import rtmidi except ImportError: rtmidi = None pass class MIDIWatcher(): def __init__(self, config, state, on_shortcut): self.config = config self.state = state self.on_shortcut = on_shortcut self.failed = False self.devices = [] try: midiin = rtmidi.RtMidiIn() lookup = {} for p in range(midiin.getPortCount()): lookup[midiin.getPortName(p)] = p for device, mapping in self.config.midi.items(): if device in lookup: mapping["port"] = lookup[device] mi = rtmidi.RtMidiIn() mi.openPort(lookup[device]) self.devices.append([device, mi]) else: if self.config.midi_info: print(f">>> no midi port found with that name ({device}) <<<") if self.config.midi_info: print("\nMIDI DEVICES:::") midiin = rtmidi.RtMidiIn() ports = range(midiin.getPortCount()) for p in ports: print(">", f"\"{midiin.getPortName(p)}\"") print("\n") except Exception as e: self.failed = True print("MIDI SETUP EXCEPTION >", e) def monitor(self, playing): if self.failed: return controllers = {} shortcut_triggered = False for device, mi in self.devices: msg = mi.getMessage(0) while msg: if self.config.midi_info: print(device, msg) if msg.isNoteOn(): # Maybe not forever? nn = msg.getNoteNumber() shortcut = self.config.midi[device]["note_on"].get(nn) if shortcut: self.on_shortcut(shortcut, nn) shortcut_triggered = True elif msg.isNoteOff(): nn = msg.getNoteNumber() elif msg.isController(): cn = msg.getControllerNumber() cv = msg.getControllerValue() cc = msg.getChannel() mapping = self.config.midi[device].get("controller", {}) shortcutA = mapping.get(cn) shortcutB = mapping.get((cn, cc)) if shortcutA: if cv in shortcutA: print("shortcut!", shortcutA, cv, shortcutA.get(cv)) self.on_shortcut(shortcutA.get(cv), cn) shortcut_triggered = True if shortcutB: if callable(shortcutB): res = shortcutB(cv, self.state) if res is not None: print("shortcut!", res) self.on_shortcut(res, None) shortcut_triggered = True else: if cv in shortcutB: print("shortcut!", shortcutB, cv) self.on_shortcut(shortcutB.get(cv), cn) shortcut_triggered = True else: controllers["_".join([device, str(cc), str(cn)])] = cv msg = mi.getMessage(0) if len(controllers) > 0: nested = {} for k, v in controllers.items(): device, channel, number = k.split("_") if not nested.get(device): nested[device] = {} if not nested[device].get(channel): nested[device][channel] = {} nested[device][channel][str(number)] = v for device, channels in nested.items(): if not self.state.controller_values.get(device): self.state.controller_values[device] = {} for channel, numbers in channels.items(): if not self.state.controller_values[device].get(channel): self.state.controller_values[device][channel] = {} for number, value in numbers.items(): was = self.state.controller_values[device][channel].get(number) if was: self.state.controller_values[device][channel]["_" + str(number)] = was self.state.controller_values[device][channel][number] = value if not playing and not shortcut_triggered: return True ================================================ FILE: packages/coldtype-core/src/coldtype/renderer/winman/passthrough.py ================================================ class WinmanPassthrough(): def __init__(self): self.context = None def set_title(self, text): #print("TITLE", text) pass def terminate(self): pass def reset(self): pass def turn_over(self): print("turning") def should_close(self): return False ================================================ FILE: packages/coldtype-core/src/coldtype/runon/__init__.py ================================================ from coldtype.runon.runon import Runon, RunonEnumerable ================================================ FILE: packages/coldtype-core/src/coldtype/runon/_path.py ================================================ # WARNING from copy import deepcopy from fontTools.pens.recordingPen import RecordingPen from fontTools.pens.reverseContourPen import ReverseContourPen from coldtype.color import Color, normalize_color from coldtype.geometry import Rect, Point, txt_to_edge from coldtype.runon.runon import Runon from coldtype.runon.scaffold import Scaffold # IMPORTS class P(Runon): """ P stands for Path (or Pen) """ def FromPens(pens): if hasattr(pens, "_pens"): out = P().data(**pens.data) for p in pens: out.append(P.FromPens(p)) elif hasattr(pens, "_els") and len(pens._els) > 0: out = pens elif hasattr(pens, "_val") and pens.val_present(): out = pens else: p = pens rp = RecordingPen() p.replay(rp) out = P(rp) attrs = p.attrs.get("default", {}) if "fill" in attrs: out.f(attrs["fill"]) if "stroke" in attrs: out.s(attrs["stroke"]["color"]) out.sw(attrs["stroke"]["weight"]) # TODO also the rest of the styles out.data(**pens.data) if hasattr(pens, "_frame"): out.data(frame=pens._frame) if hasattr(pens, "glyphName"): out.data(glyphName=pens.glyphName) return out def __init__(self, *vals, **kwargs): prenorm = [v.rect if isinstance(v, Scaffold) else v for v in vals] if len(vals) == 2 and isinstance(vals[0], float) and isinstance(vals[1], float): prenorm = Rect(*vals) if len(vals) == 1 and isinstance(vals[0], dict): unmapped = type(self)() for k, v in vals[0].items(): unmapped.append(v.tag(k)) prenorm = unmapped super().__init__(*prenorm) if isinstance(self._val, RecordingPen): pass elif isinstance(self._val, Rect): r = self._val self._val = RecordingPen() self.rect(r) elif isinstance(self._val, Point): p = self._val self._val = RecordingPen() self.rect(Rect.FromCenter(p, 20)) else: raise Exception("Can’t understand _val", self._val) # more backwards compat for k, v in kwargs.items(): if k == "fill": self.f(v) elif k == "stroke": self.s(v) elif k == "strokeWidth": self.sw(v) elif k == "image": self.img(**v) elif k == "shadow": self.shadow(**v) else: raise Exception("Invalid __init__ kwargs", k) def reset_val(self): super().reset_val() self._val = RecordingPen() return self def val_present(self): return self._val is not None and len(self._val.value) > 0 def copy_val(self, val): copy = RecordingPen() if self.val_present(): copy.value = deepcopy(self._val.value) return copy def printable_val(self): if self.val_present(): return f"{len(self._val.value)}mvs" def printable_data(self): out = {} exclude = ["_last_align_rect", "_notebook_shown"] for k, v in self._data.items(): if k not in exclude: out[k] = v return out def to_code(self, classname="P", additional_lines=[]): t = None if self._tag and self._tag != "?": t = self._tag out = f"({classname}()" if t: out += f"\n .tag(\"{t}\")" if self.data: pd = self.printable_data() if pd: out += f"\n .data(**{repr(self.printable_data())})" if self.val_present(): for mv, pts in self._val.value: out += "\n" if len(pts) > 0: spts = ", ".join([f"{(x, y)}" for (x, y) in pts]) out += f" .{mv}({spts})" else: out += f" .{mv}()" else: for pen in self: for idx, line in enumerate(pen.to_code().split("\n")): if idx == 0: out += f"\n .append{line}" else: out += f"\n {line}" if self.attrs: for k, v in self.attrs.get("default").items(): if v: if k == "fill": out += f"\n .f({v.to_code()})" elif k == "stroke": out += f"\n .s({v['color'].to_code()})" out += f"\n .sw({v['weight']})" else: print("No code", k, v) for la in additional_lines: out += f"\n {la}" out += ")" return out def normalize_attr_value(self, k, v): if k == "fill" and not isinstance(v, Color): return normalize_color(v) else: return super().normalize_attr_value(k, v) def style(self, style="_default"): """for backwards compatibility with defaults and grouped-stroke-properties""" st = {**super().style(style)} if style != "_default": default_style = {**super().style("default")} else: default_style = st return self.groupedStyle(st, default_style) def unframe(self): def _unframe(el, _, __): el.data(frame=None) return self.walk(_unframe) def frame(self, fn_or_rect): if isinstance(fn_or_rect, Rect): self.data(frame=fn_or_rect) elif callable(fn_or_rect): self.data(frame=fn_or_rect(self)) return self def pen(self, frame=True): """collapse and combine into a single vector""" if len(self) == 0: return self _frame = self.ambit() self.collapse() for el in self._els: el._val.replay(self._val) #self._val.record(el._val) try: self._attrs = {**self._els[0]._attrs, **self._attrs} except IndexError: pass if frame: self.data(frame=_frame) self._els = [] return self def down(self): return self.pen() def pens(self): if self.val_present(): return self.ups() else: return self # multi-use overrides def reverse(self, recursive=False, winding=True): """Reverse elements; if pen value present, reverse the winding direction of the pen.""" if winding and self.val_present(): if self.unended(): self.closePath() dp = RecordingPen() rp = ReverseContourPen(dp) self.replay(rp) self._val.value = dp.value return self return super().reverse(recursive=recursive, winding=winding) def index(self, idx, fn=None): if not self.val_present(): return super().index(idx, fn) return self.mod_contour(idx, fn) def indices(self, idxs, fn=None): if not self.val_present(): return super().indices(idxs, fn) def apply(idx, x, y): if idx in idxs: return fn(Point(x, y)) return self.map_points(apply) def wordPens(self, pred=lambda x: x.glyphName == "space", consolidate=False): def _wp(p): return (p .split(pred) .map(lambda x: x .data(word="/".join([p.glyphName for p in x])) .cond(consolidate, lambda p: p.pen()) )) d = self.depth() if d == 1: return _wp(self) out = type(self)() for pen in self: out.append(_wp(pen)) return out def linebreak(self, w, leading=False, track_out=False): x = 0 lines = P() line = P() lines.append(line) for idx, word in enumerate(self): word.t(-x, 0) amb = word.ambit(tx=1, ty=0) if amb.pse.x > w+0: line = P() lines.append(line) word.t(-amb.x, 0) x += amb.x line.append(word) else: # Align first word of first line to true x if idx == 0: word.t(-amb.x, 0) x += amb.x line.append(word) if leading: lines.stack(leading) if track_out: lines.map(lambda p: p.track_to_rect(Rect(0, 0, w, 100).zero(), pullToEdges=track_out < 2), slice(-1)) return lines def interpolate(self, value, other, frame=False): if len(self.v.value) != len(other.v.value): raise Exception("Cannot interpolate / diff lens") vl = [] for idx, (mv, pts) in enumerate(self.v.value): ipts = [] for jdx, p in enumerate(pts): pta = Point(p) try: ptb = Point(other.v.value[idx][-1][jdx]) except IndexError: print(">>>>>>>>>>>>> Can’t interpolate", idx, mv, "///", other.v.value[idx]) raise IndexError ipt = pta.interp(value, ptb) ipts.append(ipt) vl.append((mv, ipts)) np = type(self)() np.v.value = vl if frame: af = self.data("frame") bf = other.data("frame") ff = af.interp(value, bf) np.data(frame=ff) return np def replaceGlyph(self, glyphName, replacement, limit=None): return self.replace(lambda p: p.glyphName == glyphName, lambda p: (replacement(p) if callable(replacement) else replacement) .translate(*p.ambit().xy())) def findGlyph(self, glyphName, fn=None): return self.find(lambda p: p.glyphName == glyphName, fn) def _repr_html_(self): #if self.data("_notebook_shown"): # return None from coldtype.notebook import show, DEFAULT_DISPLAY self.ch(show(DEFAULT_DISPLAY, tx=1, ty=1)) return None def text(self, text:str, style, frame:Rect, x="mnx", y="mny", ): self.rect(frame) self.data( text=text, style=style, align=(txt_to_edge(x), txt_to_edge(y))) return self # backwards compatibility (questionable if should exist) def reversePens(self): """for backwards compatibility""" return self.reverse(recursive=False) rp = reversePens def vl(self, value): self.v.value = value return self @property def _pens(self): return self._els @property def value(self): return self.v.value @property def glyphName(self): return self.data("glyphName") def drop(self, amount, edge): amb = self.ambit(tx=1, ty=1).drop(amount, edge) return self.intersection(P(amb)) def take(self, amount, edge): amb = self.ambit(tx=1, ty=1).take(amount, edge) return self.intersection(P(amb)) def inset(self, ax, ay): amb = self.ambit(tx=1, ty=1).inset(ax, ay) return self.intersection(P(amb)) @staticmethod def Enumerate(enumerable, enumerator): return P().enumerate(enumerable, enumerator) def addFrame(self, frame): return self.data(frame=frame) def xAlignToFrame(self): return self.align(self.data("frame"), y=None) def pvl(self): for idx, (_, pts) in enumerate(self.v.value): if len(pts) > 0: self.v.value[idx] = list(self.v.value[idx]) self.v.value[idx][-1] = [Point(p) for p in self.v.value[idx][-1]] return self def dots(self, radius=4, square=False): """(Necessary?) Create circles at moveTo commands""" dp = type(self)() for t, pts in self.v.value: if t == "moveTo": x, y = pts[0] if square: dp.rect(Rect((x-radius, y-radius, radius, radius))) else: dp.oval(Rect((x-radius, y-radius, radius, radius))) self.v.value = dp.v.value return self # MIXINS def runonCast(): def _runonCast(p): return P.FromPens(p) return _runonCast ================================================ FILE: packages/coldtype-core/src/coldtype/runon/mixins/DrawingMixin.py ================================================ import math from pathlib import Path from fontTools.pens.recordingPen import RecordingPen from coldtype.geometry import Rect, Line, Point, Atom from typing import Callable class DrawingMixin(): def _normPointSplat(self, p): if isinstance(p[0], Point): return p[0].xy() elif len(p) == 1: return p[0] else: return p def moveTo(self, *p): p = self._normPointSplat(p) self._val.moveTo(p) return self def m(self, *p): return self.moveTo(*p) def lineTo(self, *p): p = self._normPointSplat(p) if len(self._val.value) == 0: self._val.moveTo(p) else: self._val.lineTo(p) return self def l(self, *p): return self.lineTo(*p) def qCurveTo(self, *points): self._val.qCurveTo(*points) return self def q(self, *p): return self.qCurveTo(*p) def curveTo(self, *points): self._val.curveTo(*points) return self def c(self, *p): return self.curveTo(*p) def closePath(self): self._val.closePath() return self def cp(self): return self.closePath() def endPath(self): self._val.endPath() return self def ep(self): return self.endPath() def addComponent(self, baseGlyphName, transformation): print("pen.addComponent('%s', %s)" % (baseGlyphName, tuple(transformation))) return self def points(self, pts, close=True): self.moveTo(pts[0]) for p in pts[1:]: self.lineTo(p) if close: self.closePath() else: self.endPath() return self def point_list(self, random_seed=None): all_pts = [] for idx, (mv, pts) in enumerate(self._val.value): all_pts.extend([Point(*p) for p in pts]) if random_seed is not None: from random import Random rnd = Random() rnd.seed(random_seed) rnd.shuffle(all_pts) return all_pts def replay(self, pen): self._val.replay(pen) for el in self._els: el.replay(pen) return self def record(self, pen): """Play a pen into this pen, meaning that pen will be added to this one’s value.""" if hasattr(pen, "value"): pen.replay(self._val) return self if len(pen) > 0: for el in pen._els: self.record(el._val) elif pen: if isinstance(pen, Path): self.withJSONValue(pen) else: pen.replay(self._val) return self def unended(self): if not self.val_present(): return None if len(self._val.value) == 0: return True elif self._val.value[-1][0] not in ["endPath", "closePath"]: return True return False def fully_close_path(self): if not self.val_present(): # TODO log noop? return self if self._val.value[-1][0] == "closePath": start = self._val.value[0][-1][-1] end = self._val.value[-2][-1][-1] if start != end: self._val.value = self._val.value[:-1] self.lineTo(start) self.closePath() return self fullyClosePath = fully_close_path def rect(self, rect): """Rectangle primitive — `moveTo/lineTo/lineTo/lineTo/closePath`""" rect = Rect(rect) self.moveTo(rect.point("SW").xy()) self.lineTo(rect.point("SE").xy()) self.lineTo(rect.point("NE").xy()) self.lineTo(rect.point("NW").xy()) self.closePath() return self r = rect def roundedRect(self, rect, hr, vr=None, scale=True): """Rounded rectangle primitive""" if vr is None: vr = hr l, b, w, h = Rect(rect) r, t = l + w, b + h K = 4 * (math.sqrt(2)-1) / 3 if scale: circle = hr == 0.5 and vr == 0.5 if hr <= 0.5: hr = w * hr if vr <= 0.5: vr = h * vr else: circle = False self.moveTo((l + hr, b)) if not circle: self.lineTo((r - hr, b)) self.curveTo((r+hr*(K-1), b), (r, b+vr*(1-K)), (r, b+vr)) if not circle: self.lineTo((r, t-vr)) self.curveTo((r, t-vr*(1-K)), (r-hr*(1-K), t), (r-hr, t)) if not circle: self.lineTo((l+hr, t)) self.curveTo((l+hr*(1-K), t), (l, t-vr*(1-K)), (l, t-vr)) if not circle: self.lineTo((l, b+vr)) self.curveTo((l, b+vr*(1-K)), (l+hr*(1-K), b), (l+hr, b)) self.closePath() return self rr = roundedRect def oval(self, rect): """Oval primitive""" if isinstance(rect, Point): self.roundedRect(Rect.FromCenter(rect, 20, 20), 0.5, 0.5) else: self.roundedRect(rect, 0.5, 0.5) return self o = oval def superellipse(self, r, factor=65): return (self .moveTo(r.pw) .bxc(r.ps, "SW", factor) .bxc(r.pe, "SE", factor) .bxc(r.pn, "NE", factor) .bxc(r.pw, "NW", factor)) def line(self, points, moveTo=True, endPath=True): """Syntactic sugar for `moveTo`+`lineTo`(...)+`endPath`; can have any number of points""" if isinstance(points, Line): points = list(points) if len(points) == 0: return self if len(self._val.value) == 0 or moveTo: self.moveTo(points[0]) else: self.lineTo(points[0]) for p in points[1:]: self.lineTo(p) if endPath: self.endPath() return self def hull(self, points): """Same as `.line` but calls closePath instead of endPath`""" self.moveTo(points[0]) for pt in points[1:]: self.lineTo(pt) self.closePath() return self def round(self): """Round the values of this pen to integer values.""" return self.round_to(1) def round_to(self, rounding): """Round the values of this pen to nearest multiple of rounding.""" def rt(v, mult): rndd = float(round(v / mult) * mult) if rndd.is_integer(): return int(rndd) else: return rndd rounded = [] for t, pts in self._val.value: _rounded = [] for p in pts: if p: x, y = p _rounded.append((rt(x, rounding), rt(y, rounding))) else: _rounded.append(p) rounded.append((t, _rounded)) self._val.value = rounded return self # Compound curve mechanics def interpCurveTo(self, p1, f1, p2, f2, to, inset=0): a = Point(self._val.value[-1][-1][-1]) d = Point(to) pl = Line(p1, p2).inset(inset) b = Line(a, pl.start).t(f1/100) c = Line(d, pl.end).t(f2/100) return self.curveTo(b, c, d) def ioc(self, pt, slope=0, fA=0, fB=85): return self.ioEaseCurveTo(pt, slope, fA, fB) def ioEaseCurveTo(self, pt, slope=0, fA=0, fB=85): a = Point(self._val.value[-1][-1][-1]) d = Point(pt) box = Rect.FromMnMnMxMx([ min(a.x, d.x), min(a.y, d.y), max(a.x, d.x), max(a.y, d.y) ]) if a.y < d.y: line_vertical = Line(box.ps, box.pn) else: line_vertical = Line(box.pn, box.ps) angle = Line(a, d).angle() - line_vertical.angle() try: fA1, fA2 = fA except TypeError: fA1, fA2 = fA, fA try: fB1, fB2 = fB except TypeError: fB1, fB2 = fB, fB rotated = line_vertical.rotate(math.degrees(angle*(slope/100))) vertical = Line(rotated.intersection(box.es), rotated.intersection(box.en)) if a.y > d.y: vertical = vertical.reverse() c1 = Line(a, vertical.start).t(fA1) c2 = Line(vertical.mid, vertical.start).t(fA1) self.lineTo(c1) self.curveTo( Line(c1, vertical.start).t(fB1), Line(c2, vertical.start).t(fB1), c2) c1 = Line(vertical.mid, vertical.end).t(fA2) c2 = Line(d, vertical.end).t(fA2) self.lineTo(c1) self.curveTo( Line(c1, vertical.end).t(fB2), Line(c2, vertical.end).t(fB2), c2) self.lineTo(d) return self def bxc(self, pt, point, factor=0.65, po=(0, 0), mods={}, flatten=False): return self.boxCurveTo(pt, point, factor, po, mods, flatten) def roundedCorner(self, pt, point, multipliers, offset=4, factor=65): a, b, c, d = multipliers return (self .lineTo(pt.offset(offset*a, offset*b)) .boxCurveTo(pt.offset(offset*c, offset*d), point, factor=factor)) def boxCurveTo(self, pt, point, factor=0.65, po=(0, 0), mods={}, flatten=False): if flatten: self.lineTo(pt) return self a = Point(self._val.value[-1][-1][-1]) d = Point(pt) box = Rect.FromMnMnMxMx([ min(a.x, d.x), min(a.y, d.y), max(a.x, d.x), max(a.y, d.y) ]) try: f1, f2 = factor except TypeError: if isinstance(factor, Atom): f1, f2 = (factor[0], factor[0]) else: f1, f2 = (factor, factor) if isinstance(point, str): #print("POINT", point) if point == "cx": # ease-in-out if a.y < d.y: p1 = box.pse p2 = box.pnw elif a.y > d.y: p1 = box.pne p2 = box.psw else: p1 = p2 = a.interp(0.5, d) elif point == "e": # ease-in if a.y < d.y: p1 = p2 = box.pse elif a.y > d.y: p1 = p2 = box.pne else: p1 = p2 = a.interp(0.5, d) elif point == "w": # ease-out if a.y < d.y: p1 = p2 = box.pnw elif a.y > d.y: p1 = p2 = box.psw else: p1 = p2 = a.interp(0.5, d) else: if "," in point: pt1, pt2 = [x.strip() for x in point.split(",")] p1 = box.point(pt1) p2 = box.point(pt2) else: p = box.point(point) p1, p2 = (p, p) elif isinstance(point, Point): p1, p2 = point, point else: p1, p2 = point p1 = box.point(p1) p2 = box.point(p2) p1 = p1.offset(*po) p2 = p2.offset(*po) b = a.interp(f1, p1) c = d.interp(f2, p2) mb = mods.get("b") mc = mods.get("c") if mb: b = mb(b) elif mc: c = mc(c) self.curveTo(b, c, d) return self def mirror(self, factors, point=None): if point == 0: point = (0, 0) return (self.layer(1, lambda p: p.scale(*factors, point=point or self.ambit().psw))) def mirrorx(self, point=None): return self.mirror((-1, 1), point=point) def mirrory(self, point=None): return self.mirror((1, -1), point=point) def mirrorxy(self, point=None): return self.mirror((-1, -1), point=point) def pattern(self, rect, clip=False): dp_copy = self.copy() #dp_copy.value = self.value for y in range(-1, 1): for x in range(-1, 1): dpp = type(self)() dp_copy.replay(dpp) dpp.translate(rect.w*x, rect.h*y) dpp.replay(self) self.translate(rect.w/2, rect.h/2) if clip: clip_box = type(self)().rect(rect) return self.intersection(clip_box) return self def withRect(self, rect, fn:Callable[[Rect, "P"], "P"]) -> "P": r = Rect(rect) return fn(r, self).data(frame=r) def gridlines(self, rect, x=20, y=None, absolute=False): """Construct a grid in the pen using `x` and (optionally) `y` subdivisions""" xarg = x yarg = y or x if absolute: x = int(rect.w / xarg) y = int(rect.h / yarg) else: x = xarg y = yarg for _x in rect.subdivide(x, "minx"): if _x.x > 0 and _x.x > rect.x: self.line([_x.point("NW"), _x.point("SW")]) for _y in rect.subdivide(y, "miny"): if _y.y > 0 and _y.y > rect.y: self.line([_y.point("SW"), _y.point("SE")]) return self.f(None).s(0, 0.1).sw(3) def ez(self, r, start_y, end_y, s): self.moveTo(r.edge("W").t(start_y)) self.gs(s, do_close=False, first_move="lineTo") self.lineTo(r.edge("E").t(end_y)) self.endPath() return self def segments(self, all_curves=False): if not self.val_present(): for idx, el in enumerate(self._els): self._els[idx] = el.segments() return self segs = [] last = None for contour in self.copy().explode(): for mv, pts in contour.v.value: if last: if mv == "curveTo": segs.append(type(self)().moveTo(last).curveTo(*pts)) if mv == "lineTo": if all_curves: ln = Line(last, pts[0]) segs.append(type(self)().moveTo(ln.start).curveTo(ln.t(0.25), ln.t(0.75), ln.end)) else: segs.append(type(self)().moveTo(last).lineTo(*pts)) if len(pts) > 0: last = pts[-1] else: last = None self._val = None self._els = segs return self def join(self): self._val = RecordingPen() self._val.moveTo(self._els[0].v.value[0][-1][-1]) for el in self._els: self._val.value.extend(el.v.value[1:]) self._els = [] return self def substructure(self): indicators = type(self)() def append(p): substructure = p.data("substructure") if substructure: indicators.append(substructure) self.mapv(append) return indicators ================================================ FILE: packages/coldtype-core/src/coldtype/runon/mixins/FXMixin.py ================================================ import math from copy import deepcopy from random import randint from fontTools.pens.basePen import decomposeQuadraticSegment from fontTools.pens.recordingPen import RecordingPen from fontPens.flattenPen import FlattenPen from coldtype.geometry import Point from coldtype.pens.outlinepen import OutlinePen from coldtype.pens.translationpen import TranslationPen, polarCoord from coldtype.pens.misc import ExplodingPen, SmoothPointsPen from coldtype.random import random_series class FXMixin(): def trim_start(self): self.pvl() new_start = self._val.value[1][-1][-1] self._val.value[1][0] = "moveTo" self._val.value[1][-1] = [new_start] self._val.value = self._val.value[1:] return self def trim_end(self): self.pvl() end = self._val.value[-1][0] if end in ["closePath", "endPath"]: self._val.value = self._val.value[:-2] if end == "closePath": self.cp() else: self.ep() else: self._val.value = self._val.value[:-1] return self def q2c(self): new_vl = [] for mv, pts in self.v.value: if mv == "qCurveTo": # does not handle all-offcurve+None mode # https://forum.drawbot.com/topic/58/qcurve # https://github.com/fonttools/skia-pathops/issues/45 # https://github.com/fonttools/skia-pathops/issues/71 decomposed = decomposeQuadraticSegment(pts) for dpts in decomposed: qp1, qp2 = [Point(pt) for pt in dpts] try: qp0 = Point(new_vl[-1][-1][-1]) cp1 = qp0 + (qp1 - qp0)*(2.0/3.0) cp2 = qp2 + (qp1 - qp2)*(2.0/3.0) new_vl.append(["curveTo", (cp1, cp2, qp2)]) except Exception as e: print("failed q2c", e) else: new_vl.append([mv, pts]) self.v.value = new_vl return self def flatten(self, length=10, segmentLines=True): """ Runs a fontTools `FlattenPen` on this pen """ for el in self._els: el.flatten(length, segmentLines) if self.val_present(): rp = RecordingPen() fp = FlattenPen(rp, approximateSegmentLength=length, segmentLines=segmentLines) self.replay(fp) self._val.value = rp.value return self def smooth(self): for el in self._els: el.smooth() if self.val_present(): rp = RecordingPen() fp = SmoothPointsPen(rp) self.replay(fp) self._val.value = rp.value return self def catmull(self, points, close=False): """Run a catmull spline through a series of points""" p0 = points[0] p1, p2, p3 = points[:3] pts = [p0] i = 1 while i < len(points): pts.append([ ((-p0[0] + 6 * p1[0] + p2[0]) / 6), ((-p0[1] + 6 * p1[1] + p2[1]) / 6), ((p1[0] + 6 * p2[0] - p3[0]) / 6), ((p1[1] + 6 * p2[1] - p3[1]) / 6), p2[0], p2[1] ]) p0 = p1 p1 = p2 p2 = p3 try: p3 = points[i + 2] except: p3 = p3 i += 1 self.moveTo(pts[0]) for p in pts[1:]: self.curveTo((p[0], p[1]), (p[2], p[3]), (p[4], p[5])) if close: self.closePath() return self def roughen(self, amplitude=10, threshold=10, ignore_ends=False, seed=None): """Randomizes points in skeleton""" if seed is not None: rs = random_series(0, amplitude, seed=seed) else: rs = random_series(0, amplitude, seed=randint(0, 5000)) randomized = [] _x = 0 _y = 0 for idx, (t, pts) in enumerate(self.v.value): if idx == 0 and ignore_ends: randomized.append([t, pts]) continue if idx == len(self.v.value) - 1 and ignore_ends: randomized.append([t, pts]) continue if t == "lineTo" or t == "curveTo": #jx = pnoise1(_x) * amplitude # should actually be 1-d on the tangent (maybe? TODO) #jy = pnoise1(_y) * amplitude jx = rs[idx*2] - amplitude/2 jy = rs[idx*2+1] - amplitude/2 randomized.append([t, [(x+jx, y+jy) for x, y in pts]]) _x += 0.2 _y += 0.3 else: randomized.append([t, pts]) self.v.value = randomized return self def explode(self): """Convert all contours to individual paths""" for el in self._els: el.explode() if self.val_present(): rp = RecordingPen() ep = ExplodingPen(rp) self.replay(ep) for p in ep._pens: el = type(self)() el._val.value = p el._attrs = deepcopy(self._attrs) self.append(el) self._val = RecordingPen() return self def implode(self): # TODO preserve frame from some of this? #self.reset_val() self._val = RecordingPen() for el in self._els: self.record(el._val) self._els = [] return self def map_points(self, fn, filter_fn=None): idx = 0 for cidx, c in enumerate(self._val.value): move, pts = c pts = list(pts) for pidx, p in enumerate(pts): x, y = p if filter_fn and not filter_fn(Point(p)): continue result = fn(idx, x, y) if result: pts[pidx] = result idx += 1 self._val.value[cidx] = (move, pts) return self def mod_contour(self, contour_index, mod_fn=None): exploded = self.copy().explode() if mod_fn: mod_fn(exploded[contour_index]) self._val.value = exploded.implode()._val.value return self else: return exploded[contour_index] def filterContours(self, filter_fn): if self.val_present(): exploded = self.copy().explode() keep = [] for idx, c in enumerate(exploded): if filter_fn(idx, c): keep.append(c) self._val.value = type(self)(keep).implode()._val.value return self def repeat(self, times=1): for el in self._els: el.repeat(times) if self.val_present(): copy = self.copy()._val.value _, copy_0_data = copy[0] copy[0] = ("moveTo", copy_0_data) self._val.value = self._val.value[:-1] + copy if times > 1: self.repeat(times-1) return self def outline(self, offset=1, drawInner=True, drawOuter=True, cap="square", miterLimit=None, closeOpenPaths=True ): """AKA expandStroke""" for el in self._els: el.outline(offset, drawInner, drawOuter, cap, miterLimit, closeOpenPaths) if self.val_present(): op = OutlinePen(None , offset=offset , optimizeCurve=True , cap=cap , miterLimit=miterLimit , closeOpenPaths=closeOpenPaths) self._val.replay(op) op.drawSettings(drawInner=drawInner , drawOuter=drawOuter) g = op.getGlyph() self._val.value = [] g.draw(self._val) return self ol = outline def project(self, angle, width): offset = polarCoord((0, 0), math.radians(angle), width) self.translate(offset[0], offset[1]) return self def castshadow(self, angle=-45, width=100, ro=1, fill=True ): for el in self._els: el.castshadow(angle, width, ro, fill) if self.val_present(): out = RecordingPen() tp = TranslationPen(out , frontAngle=angle , frontWidth=width) self._val.replay(tp) if fill: self.copy().project(angle, width)._val.replay(out) #out.record() self._val.value = out.value if ro: self.removeOverlap() return self def understroke(self, s=0, sw=5, outline=False, dofill=0, miterLimit=None ): if sw == 0: return self def mod_fn(p): if not outline: return p.fssw(s, s, sw) else: if dofill: pf = p.copy() p.f(s).outline(sw*2, miterLimit=miterLimit) if dofill: p.reverse().record(pf) return p return self.layerv(mod_fn, 1) ================================================ FILE: packages/coldtype-core/src/coldtype/runon/mixins/GeometryMixin.py ================================================ from coldtype.geometry.point import Point from coldtype.geometry.line import Line class GeometryMixin(): def nsew(self): pts = [el[1][-1] for el in self.v.value if len(el[1]) > 0] lines = [] for i, p in enumerate(pts): if i + 1 == len(pts): lines.append(Line(p, pts[0])) else: lines.append(Line(p, pts[i+1])) mnx, mny, mxx, mxy = self.bounds().mnmnmxmx() min_ang = min([l.ang for l in lines]) max_ang = max([l.ang for l in lines]) #for idx, l in enumerate(lines): # print(idx, ">", l.ang, min_ang, max_ang) xs = [l for l in lines if l.ang < 0.25 or l.ang > 2.5] ys = [l for l in lines if 1 < l.ang < 2] if len(ys) == 2 and len(xs) < 2: xs = [l for l in lines if l not in ys] elif len(ys) < 2 and len(xs) == 2: ys = [l for l in lines if l not in xs] #for l in ys: # print(l.ang) #print(len(xs), len(ys)) #print("--------------------") try: n = [l for l in xs if l.start.y == mxy or l.end.y == mxy][0] s = [l for l in xs if l.start.y == mny or l.end.y == mny][0] e = [l for l in ys if l.start.x == mxx or l.end.x == mxx][0] w = [l for l in ys if l.start.x == mnx or l.end.x == mnx][0] return n, s, e, w except IndexError: amb = self.ambit(tx=1, ty=1) return [amb.en, amb.es, amb.ee, amb.ew] def avg(self): self.pvl() pts = [] for _, _pts in self.v.value: if len(_pts) > 0: pts.extend(_pts) n = len(pts) return Point( sum([p.x for p in pts])/n, sum([p.y for p in pts])/n) @property def ecx(self): n, s, e, w = self.nsew() return e.interp(0.5, w.reverse()) @property def ecy(self): n, s, e, w = self.nsew() return n.interp(0.5, s.reverse()) def edge(self, e): e = e.lower() if e == "n": return self.en elif e == "s": return self.es elif e == "e": return self.ee elif e == "w": return self.ew def point(self, pt): n, s, e, w = self.nsew() if pt == "NE": return n.pe elif pt == "NW": return n.pw elif pt == "SE": return s.pe elif pt == "SW": return s.pw elif pt == "N": return n.mid elif pt == "S": return s.mid elif pt == "E": return e.mid elif pt == "W": return w.mid elif pt == "C": return self.ecx.sect(self.ecy) @property def pne(self): return self.point("NE") @property def pnw(self): return self.point("NW") @property def psw(self): return self.point("SW") @property def pse(self): return self.point("SE") @property def pn(self): return self.point("N") @property def ps(self): return self.point("S") @property def pe(self): return self.point("E") @property def pw(self): return self.point("W") @property def pc(self): return self.point("C") @property def en(self): return self.nsew()[0] @property def es(self): return self.nsew()[1] @property def ee(self): return self.nsew()[2] @property def ew(self): return self.nsew()[3] ================================================ FILE: packages/coldtype-core/src/coldtype/runon/mixins/GlyphMixin.py ================================================ from fontTools.pens.recordingPen import RecordingPen from fontTools.pens.transformPen import TransformPen from fontTools.misc.transform import Transform class GlyphMixin(): def glyph(self, glyph, glyphSet=None, layerComponents=False): """Play a glyph (like from `defcon`) into this pen.""" out = type(self)() base = type(self)() out.append(base) glyph.draw(base._val) new_val = [] for mv, pts in base._val.value: if mv == "addComponent": component_name, matrix = pts rp = RecordingPen() tp = TransformPen(rp, Transform(*matrix)) component = glyphSet[component_name] # recursively realize any nested components realized = type(self)().glyph(component, glyphSet) realized.replay(tp) p = type(self)() p._val = rp out.append(p) if "addComponent" in str(p._val.value): print("> NESTED COMPONENT", component_name) else: new_val.append((mv, pts)) base._val.value = new_val if layerComponents: return out else: try: out.pen().replay(self._val) except IndexError: pass return self def toGlyph(self, name=None, width=None, allow_blank=False): """ Create a glyph (like from `defcon`) using this pen’s value. *Warning*: if path is unended, closedPath will be called """ from defcon import Glyph if not allow_blank: if self.unended(): self.closePath() bounds = self.bounds() glyph = Glyph() glyph.name = name glyph.width = width or bounds.w try: sp = glyph.getPen() self.replay(sp) except AssertionError: if not allow_blank: print(">>>blank glyph:", glyph.name) return glyph ================================================ FILE: packages/coldtype-core/src/coldtype/runon/mixins/LayoutMixin.py ================================================ import math from fontTools.pens.boundsPen import BoundsPen from fontTools.misc.transform import Transform from fontTools.pens.transformPen import TransformPen from fontTools.pens.recordingPen import RecordingPen from coldtype.geometry import Point, Rect, align from coldtype.interpolation import norm from coldtype.color import bw, rgb, hsl from functools import partialmethod THTV_WARNING = False class LayoutMixin(): def bounds(self): """Calculate the exact bounds of this shape, using a BoundPen""" b = Rect(0, 0, 0, 0) if self.val_present(): try: cbp = BoundsPen(None) self._val.replay(cbp) mnx, mny, mxx, mxy = cbp.bounds b = Rect((mnx, mny, mxx - mnx, mxy - mny)) except: pass if len(self._els) > 0: bs = [] for el in self._els: eb = el.bounds() if eb and eb.nonzero(): bs.append(eb) if len(bs) > 0: b = bs[0] for eb in bs[1:]: b = b.union(eb) return b def _normT(self, th, tv, tx, ty, t): import traceback global THTV_WARNING if th is not None: #traceback.print_stack() tx = th if not THTV_WARNING: print("! API CHANGE: th/tv are now tx/ty !") THTV_WARNING = True if tv is not None: #traceback.print_stack() ty = tv if not THTV_WARNING: print("! API CHANGE: th/tv are now tx/ty !") THTV_WARNING = True if t is not None: tx = bool(int(t)) if tx: ty = int((t-1)*10) == 1 else: ty = int(t)*10 == 1 else: tx, ty = tx, ty return tx, ty def empty(self): return len(self._val.value) == 0 def ambit(self, th=None, tv=None, tx=0, ty=0, t=None) -> Rect: """ Get the calculated rect boundary. - `tx` means `(t)rue (x)` (i.e. the true width/horizontal dimension (was previously th)); - `ty` means `(t)rue (y)` (i.e. the true height/vertical dimension (was previously tv)); Passing either ignores a non-bounds-derived frame in either dimension """ tx, ty = self._normT(th, tv, tx, ty, t) f = self._data.get("frame", None) # true bounds if tx and ty: return self.bounds() # true no-bounds elif not tx and not ty and f: return f # partial bounds elif f and (self.val_present() or (self.data("glyphName") and len(self) == 0)): if self.empty(): if tx: f = f.setw(0) elif ty: f = f.seth(0) return f else: b = self.bounds() if tx: return Rect(b.x, f.y, b.w, f.h) else: return Rect(f.x, b.y, f.w, b.h) # pass-to-els elif len(self._els) > 0: try: union = self._els[0].ambit(tx=tx, ty=ty) for p in self._els[1:]: a = p.ambit(tx=tx, ty=ty) if a.x == 0 and a.y == 0 and a.w == 0 and a.h == 0: continue union = union.union(a) return union except Exception as _: return Rect(0,0,0,0) # catch-all return self.bounds() # if f or self._val: # if (th or tv) and not self.empty(): # b = self.bounds() # if th and tv: # return b # elif th: # return Rect(b.x, f.y, b.w, f.h) # else: # return Rect(f.x, b.y, f.w, b.h) # else: # if self.empty(): # if th: # f = f.setw(0) # elif tv: # f = f.seth(0) # return f # return f # elif : # return self.bounds() getFrame = ambit def align(self, rect, x="mdx", y="mdy", th=None, # deprecated tv=None, # deprecated tx=1, ty=0, transformFrame=True, h=None, returnOffset=False ): """ Align this pen to another rect, defaults to the center. - `tx` means true-x (i.e. will disregard any invisible 'frame' set on the pen (as in the case of glyphs returned from StSt/Glyphwise)); - `ty` means true-y, which is the same but for the vertical dimension """ if not isinstance(rect, Rect): if hasattr(rect, "ambit"): rect = rect.ambit(tx=tx, ty=ty) elif isinstance(rect, Point) and rect._rect is not None: x, y = rect._corner rect = rect._rect elif hasattr(rect, "rect"): rect = rect.rect else: raise Exception("can't align to this object") tx, ty = self._normT(th, tv, tx, ty, None) r = self.ambit(tx=tx, ty=ty) if h is not None: r = r.seth(h) self.data(_last_align_rect=rect) offset = align(r, rect, x, y) self.translate(*offset, transformFrame=transformFrame) if returnOffset: return offset else: return self def _align_compass(self, compass, rect, tx=1, ty=0): return self.align(rect, compass, tx=tx, ty=ty) #å = align alne = partialmethod(_align_compass, "NE") ale = partialmethod(_align_compass, "E") alse = partialmethod(_align_compass, "SE") als = partialmethod(_align_compass, "S") alsw = partialmethod(_align_compass, "SW") alw = partialmethod(_align_compass, "W") alnw = partialmethod(_align_compass, "NW") aln = partialmethod(_align_compass, "N") def xalign(self, rect=None, x="centerx", th=None, tv=None, tx=1, ty=0): tx, ty = self._normT(th, tv, tx, ty, None) if x == "C": x = "CX" if rect is None: rect = self.ambit(tx=tx, ty=ty) if callable(rect): rect = rect(self) self.align(rect, x=x, y=None, tx=tx, ty=ty) for el in self._els: el.align(rect, x=x, y=None, tx=tx, ty=ty) return self #xå = xalign def yalign(self, rect=None, y="centery", th=None, tv=None, tx=0, ty=1): tx, ty = self._normT(th, tv, tx, ty, None) if rect is None: rect = self.ambit(tx=tx, ty=ty) if callable(rect): rect = rect(self) self.align(rect, x=None, y=y, tx=tx, ty=ty) return self #yå = yalign def _normPoint(self, point=None, th=None, tv=None, tx=0, ty=0, **kwargs): tx, ty = self._normT(th, tv, tx, ty, kwargs.get("t")) if "pt" in kwargs: point = kwargs["pt"] a = self.ambit(tx=tx, ty=ty) if point is None: return a.pc elif point == 0: return a.psw elif point is False: return Point(0, 0) elif isinstance(point, str): if point.startswith("tx"): a = self.ambit(tx=1, ty=0) point = point[2:] elif point.startswith("ty"): a = self.ambit(tx=0, ty=1) point = point[2:] elif point.startswith("t"): a = self.ambit(tx=1, ty=1) point = point[1:] return a.point(point) elif (not (isinstance(point[1], int) or isinstance(point[1], float)) and hasattr(self, "_normPoint")): return self[point[0]]._normPoint(point[1]) else: return Point(point) def transform(self, transform, transformFrame=True): """Perform an arbitrary transformation on the pen, using the fontTools `Transform` class.""" if self.val_present(): op = RecordingPen() tp = TransformPen(op, transform) self._val.replay(tp) self._val.value = op.value f = self._data.get("frame") if transformFrame and f: self.data(frame=f.transform(transform)) for p in self._els: p.transform(transform, transformFrame=transformFrame) substructure = self._data.get("substructure") if substructure: substructure.transform(transform, transformFrame=transformFrame) img = self.img() if img: img["rect"] = img["rect"].transform(transform) return self def matrix(self, a, b, c, d, e, f, transformFrame=False): return self.transform(Transform(a, b, c, d, e, f), transformFrame=transformFrame) def invertYAxis(self, height): rp = RecordingPen() tp = TransformPen(rp, (1, 0, 0, -1, 0, height)) self.replay(tp) self._val.value = rp.value return self def nonlinear_transform(self, fn): for el in self._els: el.nonlinear_transform(fn) if self.val_present(): for idx, (move, pts) in enumerate(self._val.value): if len(pts) > 0: _pts = [] for _pt in pts: x, y = _pt _pts.append(fn(x, y)) self._val.value[idx] = (move, _pts) return self nlt = nonlinear_transform def translate(self, x, y=None, transformFrame=True): """Translate this shape by `x` and `y` (pixel values).""" if y is None: y = x return self.transform(Transform(1, 0, 0, 1, x, y), transformFrame=transformFrame) offset = translate t = translate def shift(self, dx, dy, tx=1, ty=1): amb = self.ambit(tx=tx, ty=ty) self.translate(amb.w*dx, amb.h*dy) return self sh = shift def zero(self, th=None, tv=None, tx=0, ty=0): tx, ty = self._normT(th, tv, tx, ty, None) x, y, _, _ = self.ambit(tx=tx, ty=ty) self.translate(-x, -y) return self def centerZero(self, th=None, tv=None, tx=0, ty=0): tx, ty = self._normT(th, tv, tx, ty, None) x, y, w, h = self.ambit(tx=tx, ty=ty) nx, ny = -x-w/2, -y-h/2 return (self .t(-x-w/2, -y-h/2) .data(centerZeroOffset=(nx, ny))) def centerPoint(self, rect, pt, interp=1, th=None, tv=None, tx=1, ty=0, **kwargs): tx, ty = self._normT(th, tv, tx, ty, None) if "i" in kwargs: interp = kwargs["i"] x, y = self._normPoint(pt, tx=tx, ty=ty, **kwargs) return self.translate(norm(interp, 0, rect.w/2-x), norm(interp, 0, rect.h/2-y)) def skew(self, x=0, y=0, point=None, th=None, tv=None, tx=1, ty=0, **kwargs): tx, ty = self._normT(th, tv, tx, ty, None) t = Transform() px, py = self._normPoint(point, tx=tx, ty=ty, **kwargs) t = t.translate(px, py) t = t.skew(x, y) t = t.translate(-px, -py) return self.transform(t) def rotate(self, degrees, point=None, th=None, tv=None, tx=1, ty=1, **kwargs): """Rotate this shape by a degree (in 360-scale, counterclockwise).""" tx, ty = self._normT(th, tv, tx, ty, None) t = Transform() x, y = self._normPoint(point, tx=tx, ty=ty, **kwargs) t = t.translate(x, y) t = t.rotate(math.radians(degrees)) t = t.translate(-x, -y) return self.transform(t, transformFrame=False) rt = rotate def r90(self, multiplier, point=None, tx=1, ty=1, **kwargs): return self.rotate(90*multiplier, point=point, tx=tx, ty=ty, **kwargs) def scale(self, scaleX, scaleY=None, point=None, th=None, tv=None, tx=1, ty=0, **kwargs): """Scale this shape by a percentage amount (1-scale).""" tx, ty = self._normT(th, tv, tx, ty, None) t = Transform() x, y = self._normPoint(point, tx=tx, ty=ty, **kwargs) if point is not False: t = t.translate(x, y) t = t.scale(scaleX, scaleY or scaleX) if point is not False: t = t.translate(-x, -y) return self.transform(t) def flipx(self): return self.scale(-1,1) def flipy(self): return self.scale(1,-1) def scaleToRect(self, rect, preserveAspect=True, shrink_only=False, tx=1, ty=0, return_number=False): """Scale this shape into a `Rect`.""" bounds = self.bounds() if not bounds.nonzero(): return self h = rect.w / bounds.w v = rect.h / bounds.h if preserveAspect: scale = h if h < v else v if shrink_only and scale >= 1: if return_number: return 1 return self if return_number: return scale else: return self.scale(scale, tx=tx, ty=ty) else: if shrink_only and (h >= 1 or v >= 1): if return_number: return 1, 1 return self if return_number: return h, v return self.scale(h, v, tx=tx, ty=ty) def scaleToWidth(self, w, shrink_only=False): """Scale this shape horizontally""" b = self.bounds() if shrink_only and b.w < w: return self else: return self.scale(w / self.bounds().w, 1) def scaleToHeight(self, h, shrink_only=False): """Scale this shape horizontally""" b = self.bounds() if shrink_only and b.h < h: return self return self.scale(1, h / self.bounds().h) # multi-elements def distribute(self, v=False, tracks=None, th=None, tv=None, tx=0, ty=0): tx, ty = self._normT(th, tv, tx, ty, None) off = 0 for idx, p in enumerate(self): if tracks is not None and idx > 0: t = tracks[idx-1] #print(t) off += t frame = p.ambit(tx=tx, ty=ty) if v: if frame.y < 0: p.translate(0, -frame.y) p.translate(0, off) off += frame.h else: if frame.x < 0: p.translate(-frame.x, 0) if frame.x > 0 and th: p.translate(-frame.x, 0) p.translate(off, 0) off += frame.w return self def spread(self, tracking=0, tx=0, zero=False): "Horizontal distribution of elements" if zero: for p in self: p.zero(tx=tx) ambits = [p.ambit(tx=tx, ty=0).expand(tracking, "E") for p in self._els] ax = 0 for idx, p in enumerate(self._els): aw = ambits[idx].w p.translate(ax, 0) ax += aw return self def stack(self, leading=0, ty=0, zero=False): "Vertical distribution of elements" if isinstance(leading, str) and "%" in leading: leading = self[0].ambit(ty=0).h * float(leading[:-1])/100 if zero: for p in self: p.zero() ambits = [p.ambit(tx=0, ty=ty).expand(leading, "N") for p in self._els] for idx, p in enumerate(self._els): for a in ambits[idx+1:]: p.translate(0, a.h) return self def track(self, t, v=False): """Track-out/distribute elements""" for idx, p in enumerate(self._els): if v: p.translate(0, -t*idx) else: p.translate(t*idx, 0) return self def lead(self, leading): "Vertical spacing" ln = len(self._els) try: if self._els[-1].ambit().y > self._els[0].ambit().y: leading = -leading except IndexError: pass for idx, p in enumerate(self._els): p.translate(0, leading*(ln-1-idx)) return self def grid(self, every, spread=0, stack=0, zero=False): top = type(self)() row = None for idx, p in enumerate(self._els): if zero: p.zero() if idx%every == 0: row = type(self)() top.append(row) row.append(p) self._els = top._els for row in self: row.spread(spread) self.stack(stack) return self def gridlayer(self, nx, ny=None, track=0, lead=0): """Spread nx copies and then stack ny copies, w/ optional tracking & leading""" return (self .layer(nx) .spread(track) .layer(ny if ny is not None else nx) .stack(lead)) def pasteup(self, styler=lambda p: p.f(bw(1)), padding=(5, 5), tx=1, ty=0, x="CX", y="CY"): r = self.ambit(tx=tx, ty=ty).inset(*[-x for x in padding]).zero() board = type(self)(r).ch(styler) self.align(r, tx=tx, ty=ty, x=x, y=y) return self.up().insert(0, board) def pattern_repeat(self, r): a = self.ambit(tx=1, ty=1) copies = type(self)() def repeater(_p): if a.mxx > r.mxx: copies.append(_p.copy().translate(-r.w, 0).fssw(hsl(0, a=0.5), -1, 0).rotate(0)) if a.mxy > r.mxy: copies.append(_p.copy().translate(0, -r.h).fssw(hsl(0.25, a=0.5), -1, 0).rotate(0)) if a.mny < r.mny: copies.append(_p.copy().translate(0, r.h).fssw(hsl(0.5, a=0.5), -1, 0).rotate(0)) if a.mnx < r.mnx: copies.append(_p.copy().translate(r.w, 0).fssw(hsl(0.75, a=0.5), -1, 0).rotate(0)) repeater(self) for c in list(copies): repeater(c) return self.up().append(copies) def track_with_width(self, t): """Track-out/distribute elements""" x = 0 for idx, p in enumerate(self._els): frame = p.ambit() p.translate(x + t, 0) x += frame.w return self def track_to_width(self, width, pullToEdges=False, r=0): return self.track_to_rect(Rect(width, 0), pullToEdges=pullToEdges, r=r) def track_to_rect(self, rect, pullToEdges=False, r=0): """Distribute pens evenly within a frame""" if len(self) == 1: return self.align(rect) total_width = 0 pens = self._els if r: pens = list(reversed(pens)) start_x = pens[0].ambit(tx=pullToEdges).x end_x = pens[-1].ambit(tx=pullToEdges).point("SE").x # TODO easy to knock out apostrophes here based on a callback, last "actual" frame total_width = end_x - start_x leftover_w = rect.w - total_width tracking_value = leftover_w / (len(self)-1) if pullToEdges: xoffset = rect.x - pens[0].bounds().x else: xoffset = rect.x - pens[0].ambit().x for idx, p in enumerate(pens): if idx == 0: p.translate(xoffset, 0) else: p.translate(xoffset+tracking_value*idx, 0) return self trackToRect = track_to_rect def connect(self, *others): return (type(self)([self, *others]) .distribute() .pen()) @property def x(self): return self.ambit().x @property def y(self): return self.ambit().y @property def w(self): return self.ambit().w @property def h(self): return self.ambit().h @property def tx(self): return self.ambit(tx=1).x @property def ty(self): return self.ambit(ty=1).y @property def tw(self): return self.ambit(tx=1).w @property def th(self): return self.ambit(ty=1).h ================================================ FILE: packages/coldtype-core/src/coldtype/runon/mixins/PathopsMixin.py ================================================ from coldtype.pens.misc import BooleanOp, calculate_pathop class PathopsMixin(): def _pathop(self, otherPen=None, operation=BooleanOp.XOR, use_skia_pathops_draw=True): if callable(otherPen): otherPen = otherPen(self) if self.val_present(): self._val.value = calculate_pathop(self, otherPen, operation, use_skia_pathops_draw=use_skia_pathops_draw) if otherPen is not None or operation == BooleanOp.Simplify: for el in self._els: el._pathop(otherPen, operation) else: curr = self._els[0] for el in self._els[1:]: curr._pathop(el, operation) self._els = [curr] # if hasattr(self, "pmap"): # return self.pmap(lambda p: p._pathop(otherPen, operation)) # self.value = calculate_pathop(self, otherPen, operation) return self def difference(self, otherPen=None): """Calculate and return the difference of this shape and another.""" return self._pathop(otherPen=otherPen, operation=BooleanOp.Difference) def union(self, otherPen=None): """Calculate and return the union of this shape and another.""" return self._pathop(otherPen=otherPen, operation=BooleanOp.Union) def xor(self, otherPen=None): """Calculate and return the XOR of this shape and another.""" return self._pathop(otherPen=otherPen, operation=BooleanOp.XOR) def reverseDifference(self, otherPen=None): """Calculate and return the reverseDifference of this shape and another.""" return self._pathop(otherPen=otherPen, operation=BooleanOp.ReverseDifference) def intersection(self, otherPen=None): """Calculate and return the intersection of this shape and another.""" return self._pathop(otherPen=otherPen, operation=BooleanOp.Intersection) def removeOverlap(self, use_skia_pathops_draw=True): """Remove overlaps within this shape and return itself.""" return self._pathop(otherPen=None, operation=BooleanOp.Simplify, use_skia_pathops_draw=use_skia_pathops_draw) remove_overlap = removeOverlap ro = removeOverlap ================================================ FILE: packages/coldtype-core/src/coldtype/runon/mixins/SegmentingMixin.py ================================================ import math from fontPens.marginPen import MarginPen from fontTools.misc.transform import Transform from coldtype.geometry import Line from coldtype.beziers import CurveCutter, CurveSample, splitCubicAtT, calcCubicArcLength class SegmentingMixin(): def distribute_on_path(self, path, offset=0, cc=None, notfound=None, center=False, apply_tangent=True, baseline=0, ): if len(self) == 0: # TODO print error? return self if cc: cutter = cc else: cutter = CurveCutter(path) if center is not False: offset = (cutter.length-self.bounds().w)/2 + center limit = len(self._els) for idx, p in enumerate(self._els): f = p.ambit() bs = f.y ow = offset + f.x + f.w / 2 #if ow < 0: # if notfound: # notfound(p) if ow > cutter.length: limit = min(idx, limit) else: _p, tangent = cutter.subsegmentPoint(end=ow) x_shift = bs * math.cos(math.radians(tangent)) y_shift = bs * math.sin(math.radians(tangent)) if baseline > 1: p.translate(0, -f.h*baseline) t = Transform() t = t.translate(_p[0] + x_shift - f.x, _p[1] + y_shift - f.y) t = t.translate(f.x, f.y) if apply_tangent: t = t.rotate(math.radians(tangent-90)) else: p.data(tangent=tangent-90) t = t.translate(-f.x, -f.y) t = t.translate(-f.w*0.5) p.transform(t) if limit < len(self._els): self._els = self._els[0:limit] return self distributeOnPath = distribute_on_path def subsegment(self, start=0, end=1): """Return a subsegment of the pen based on `t` values `start` and `end`""" if not self.val_present(): return cc = CurveCutter(self) start = 0 end = end * cc.calcCurveLength() pv = cc.subsegment(start, end) self._val.value = pv return self def point_t(self, t=0.5): """Get point value for time `t`""" cc = CurveCutter(self) start = 0 tv = t * cc.calcCurveLength() p, tangent = cc.subsegmentPoint(start=0, end=tv) return p, tangent def split_t(self, t=0.5): if not self.val_present(): return a = self._val.value[0][-1][0] b, c, d = self._val.value[-1][-1] return splitCubicAtT(a, b, c, d, t) def add_pt_t(self, cuidx, t): if not self.val_present(): return cidx = 0 insert_idx = -1 c1, c2 = None, None for idx, (mv, pts) in enumerate(self._val.value): if mv == "curveTo": if cidx == cuidx: insert_idx = idx a = self._val.value[idx-1][-1][-1] b, c, d = pts c1, c2 = splitCubicAtT(a, b, c, d, t) cidx += 1 elif mv == "lineTo": if cidx == cuidx: insert_idx = idx a = self._val.value[idx-1][-1][-1] b = pts[0] l = Line(a, b) c1 = [l.t(0.5)] c2 = [b] cidx += 1 if c2: if len(c2) > 1: self._val.value[insert_idx] = ("curveTo", c1[1:]) self._val.value.insert(insert_idx+1, ("curveTo", c2[1:])) else: self._val.value[insert_idx] = ("lineTo", c1) self._val.value.insert(insert_idx+1, ("lineTo", c2)) return self def samples(self, interval=10, even=False): cc = CurveCutter(self) samples = [] length = cc.calcCurveLength() inc = 1 idx = 0 while inc < length: pt, tan = cc.subsegmentPoint(start=0, end=inc) samples.append(CurveSample(idx, pt, inc / length, tan)) inc += interval idx += 1 for i, s in enumerate(samples): next = samples[i+1] if i < len(samples)-1 else s prev = samples[i-1] if i > 0 else s s.neighbors(prev, next) return samples def onSamples(self, interval=10, even=False, fn=None): return (type(self)().enumerate(self.samples(interval=interval, even=even), lambda s: fn(self, s))) def length(self, t=1): """Get the length of the curve for time `t`""" cc = CurveCutter(self) start = 0 tv = t * cc.calcCurveLength() return tv def ease_t(self, e, tries=0): _, _, w, h = self.ambit() pen = MarginPen(None, e*w, isHorizontal=False) self.replay(pen) try: return pen.getAll()[0]/h except IndexError: # HACK for now but I guess works? #print("INDEX ERROR", e) if tries < 500: return self.ease_t(e-0.01, tries=tries+1) return 0 def divide(self, length=150, floor=True, count=None, idx=0, max=None): a = self.v.value[0][-1][-1] b, c, d = self.v.value[1][-1] l = calcCubicArcLength(a, b, c, d) if count is not None: length = l / count floor = False if l < length: if max is not None and len(self.v.value) < max: self.add_pt_t(0, 0.5) self.divide(length=length, floor=False, idx=idx+1, max=max) return self if max is not None and len(self.v.value) >= max: return self if floor: fl = math.floor(l/length) length = l/fl t = 1/(l/length) if l > length*1.5: self.add_pt_t(0, 1-t) self.divide(length=length, floor=False, idx=idx+1, max=max) elif max is not None: self.add_pt_t(0, 0.5) self.divide(length=length, floor=False, idx=idx+1, max=max) pass return self ================================================ FILE: packages/coldtype-core/src/coldtype/runon/mixins/SerializationMixin.py ================================================ import pickle, json from pathlib import Path from fontTools.pens.recordingPen import RecordingPen from coldtype.geometry import Rect class SerializationMixin(): def pickle(self, dst): dst.parent.mkdir(parents=True, exist_ok=True) fh = open(str(dst), "wb") def prune(pen, state, data): if state >= 0: if hasattr(pen, "_stst"): pen._stst = None self.walk(prune) pickle.dump(self, fh) fh.close() return self def Unpickle(self, src): if isinstance(src, str): src = Path(src) return pickle.load(open(str(src.expanduser()), "rb")) def withJSONValue(self, path, keys=None): data = json.loads(Path(path).expanduser().read_text()) if keys is not None: for key in keys: data = data[key] self._val.value = data return self def withSVG(self, svg): from fontTools.svgLib import SVGPath svg = SVGPath.fromstring(svg) rp = RecordingPen() svg.draw(rp) self._val.value = rp.value return self def withSVGFile(self, svg_file): svg_file = Path(svg_file).expanduser().absolute() from fontTools.svgLib import SVGPath svg = SVGPath.fromstring(svg_file.read_bytes()) rp = RecordingPen() svg.draw(rp) self._val.value = rp.value return self ================================================ FILE: packages/coldtype-core/src/coldtype/runon/mixins/SonificationMixin.py ================================================ import struct, wave from coldtype.geometry import Rect class SonificationMixin(): # contours? def _prep_for_wave(self, flatten=1, centered=False): prepped = self.copy() if centered: prepped.center(tx=1, ty=1) if flatten > 0: prepped.flatten(flatten) return prepped def toAudio(self, flatten=1, centered=False, loops=3, filename=None): import numpy as np from pedalboard.io import AudioFile prepped = self._prep_for_wave(flatten=flatten, centered=centered) left, right = [], [] for (_, pts) in prepped._val.value: if len(pts) > 0: left.append(pts[0][0] / 1000) right.append(pts[0][1] / 1000) audio = np.tile(np.array([left, right]), loops) if filename: with AudioFile(filename, "w", samplerate=48000, num_channels=2) as f: f.write(audio) return audio, len(left) def fromAudio(self, audio, start=500, end=9500, step=1, mult=1360, scale=2): for idx in range(start, end, step): try: x = audio[0][idx] y = audio[1][idx] self.oval(Rect.FromCenter((x*mult, y*mult), scale)) except IndexError: pass return self # def wavefile(self, flatten=1, centered=False) -> str: # #from IPython.display import Audio, display # prepped = self._prep_for_wave(flatten=flatten, centered=centered) # left, right = [], [] # for (_, pts) in prepped._val.value: # if len(pts) > 0: # left.append(pts[0][0]) # right.append(pts[0][1]) # samplesPerFrame = 200 # sampleRate = 48000.0 # hertz # filename = "test_1.wav" # obj = wave.open(filename, 'w') # obj.setnchannels(2) # obj.setsampwidth(2) # obj.setframerate(sampleRate) # for x in range(0, samplesPerFrame): # for idx, l in enumerate(left): # data = struct.pack(' 0: out = pens elif hasattr(pens, "_val") and pens.val_present(): out = pens else: p = pens rp = RecordingPen() p.replay(rp) out = P(rp) attrs = p.attrs.get("default", {}) if "fill" in attrs: out.f(attrs["fill"]) if "stroke" in attrs: out.s(attrs["stroke"]["color"]) out.sw(attrs["stroke"]["weight"]) # TODO also the rest of the styles out.data(**pens.data) if hasattr(pens, "_frame"): out.data(frame=pens._frame) if hasattr(pens, "glyphName"): out.data(glyphName=pens.glyphName) return out def __init__(self, *vals, **kwargs): prenorm = [v.rect if isinstance(v, Scaffold) else v for v in vals] if len(vals) == 2 and isinstance(vals[0], float) and isinstance(vals[1], float): prenorm = Rect(*vals) if len(vals) == 1 and isinstance(vals[0], dict): unmapped = type(self)() for k, v in vals[0].items(): unmapped.append(v.tag(k)) prenorm = unmapped super().__init__(*prenorm) if isinstance(self._val, RecordingPen): pass elif isinstance(self._val, Rect): r = self._val self._val = RecordingPen() self.rect(r) elif isinstance(self._val, Point): p = self._val self._val = RecordingPen() self.rect(Rect.FromCenter(p, 20)) else: raise Exception("Can’t understand _val", self._val) # more backwards compat for k, v in kwargs.items(): if k == "fill": self.f(v) elif k == "stroke": self.s(v) elif k == "strokeWidth": self.sw(v) elif k == "image": self.img(**v) elif k == "shadow": self.shadow(**v) else: raise Exception("Invalid __init__ kwargs", k) def reset_val(self): super().reset_val() self._val = RecordingPen() return self def val_present(self): return self._val is not None and len(self._val.value) > 0 def copy_val(self, val): copy = RecordingPen() if self.val_present(): copy.value = deepcopy(self._val.value) return copy def printable_val(self): if self.val_present(): return f"{len(self._val.value)}mvs" def printable_data(self): out = {} exclude = ["_last_align_rect", "_notebook_shown"] for k, v in self._data.items(): if k not in exclude: out[k] = v return out def to_code(self, classname="P", additional_lines=[]): t = None if self._tag and self._tag != "?": t = self._tag out = f"({classname}()" if t: out += f"\n .tag(\"{t}\")" if self.data: pd = self.printable_data() if pd: out += f"\n .data(**{repr(self.printable_data())})" if self.val_present(): for mv, pts in self._val.value: out += "\n" if len(pts) > 0: spts = ", ".join([f"{(x, y)}" for (x, y) in pts]) out += f" .{mv}({spts})" else: out += f" .{mv}()" else: for pen in self: for idx, line in enumerate(pen.to_code().split("\n")): if idx == 0: out += f"\n .append{line}" else: out += f"\n {line}" if self.attrs: for k, v in self.attrs.get("default").items(): if v: if k == "fill": out += f"\n .f({v.to_code()})" elif k == "stroke": out += f"\n .s({v['color'].to_code()})" out += f"\n .sw({v['weight']})" else: print("No code", k, v) for la in additional_lines: out += f"\n {la}" out += ")" return out def normalize_attr_value(self, k, v): if k == "fill" and not isinstance(v, Color): return normalize_color(v) else: return super().normalize_attr_value(k, v) def style(self, style="_default"): """for backwards compatibility with defaults and grouped-stroke-properties""" st = {**super().style(style)} if style != "_default": default_style = {**super().style("default")} else: default_style = st return self.groupedStyle(st, default_style) def unframe(self): def _unframe(el, _, __): el.data(frame=None) return self.walk(_unframe) def frame(self, fn_or_rect): if isinstance(fn_or_rect, Rect): self.data(frame=fn_or_rect) elif callable(fn_or_rect): self.data(frame=fn_or_rect(self)) return self def pen(self, frame=True): """collapse and combine into a single vector""" if len(self) == 0: return self _frame = self.ambit() self.collapse() for el in self._els: el._val.replay(self._val) #self._val.record(el._val) try: self._attrs = {**self._els[0]._attrs, **self._attrs} except IndexError: pass if frame: self.data(frame=_frame) self._els = [] return self def down(self): return self.pen() def pens(self): if self.val_present(): return self.ups() else: return self # multi-use overrides def reverse(self, recursive=False, winding=True): """Reverse elements; if pen value present, reverse the winding direction of the pen.""" if winding and self.val_present(): if self.unended(): self.closePath() dp = RecordingPen() rp = ReverseContourPen(dp) self.replay(rp) self._val.value = dp.value return self return super().reverse(recursive=recursive, winding=winding) def index(self, idx, fn=None): if not self.val_present(): return super().index(idx, fn) return self.mod_contour(idx, fn) def indices(self, idxs, fn=None): if not self.val_present(): return super().indices(idxs, fn) def apply(idx, x, y): if idx in idxs: return fn(Point(x, y)) return self.map_points(apply) def wordPens(self, pred=lambda x: x.glyphName == "space", consolidate=False): def _wp(p): return (p .split(pred) .map(lambda x: x .data(word="/".join([p.glyphName for p in x])) .cond(consolidate, lambda p: p.pen()) )) d = self.depth() if d == 1: return _wp(self) out = type(self)() for pen in self: out.append(_wp(pen)) return out def linebreak(self, w, leading=False, track_out=False): x = 0 lines = P() line = P() lines.append(line) for idx, word in enumerate(self): word.t(-x, 0) amb = word.ambit(tx=1, ty=0) if amb.pse.x > w+0: line = P() lines.append(line) word.t(-amb.x, 0) x += amb.x line.append(word) else: # Align first word of first line to true x if idx == 0: word.t(-amb.x, 0) x += amb.x line.append(word) if leading: lines.stack(leading) if track_out: lines.map(lambda p: p.track_to_rect(Rect(0, 0, w, 100).zero(), pullToEdges=track_out < 2), slice(-1)) return lines def interpolate(self, value, other, frame=False): if len(self.v.value) != len(other.v.value): raise Exception("Cannot interpolate / diff lens") vl = [] for idx, (mv, pts) in enumerate(self.v.value): ipts = [] for jdx, p in enumerate(pts): pta = Point(p) try: ptb = Point(other.v.value[idx][-1][jdx]) except IndexError: print(">>>>>>>>>>>>> Can’t interpolate", idx, mv, "///", other.v.value[idx]) raise IndexError ipt = pta.interp(value, ptb) ipts.append(ipt) vl.append((mv, ipts)) np = type(self)() np.v.value = vl if frame: af = self.data("frame") bf = other.data("frame") ff = af.interp(value, bf) np.data(frame=ff) return np def replaceGlyph(self, glyphName, replacement, limit=None): return self.replace(lambda p: p.glyphName == glyphName, lambda p: (replacement(p) if callable(replacement) else replacement) .translate(*p.ambit().xy())) def findGlyph(self, glyphName, fn=None): return self.find(lambda p: p.glyphName == glyphName, fn) def _repr_html_(self): #if self.data("_notebook_shown"): # return None from coldtype.notebook import show, DEFAULT_DISPLAY self.ch(show(DEFAULT_DISPLAY, tx=1, ty=1)) return None def text(self, text:str, style, frame:Rect, x="mnx", y="mny", ): self.rect(frame) self.data( text=text, style=style, align=(txt_to_edge(x), txt_to_edge(y))) return self # backwards compatibility (questionable if should exist) def reversePens(self): """for backwards compatibility""" return self.reverse(recursive=False) rp = reversePens def vl(self, value): self.v.value = value return self @property def _pens(self): return self._els @property def value(self): return self.v.value @property def glyphName(self): return self.data("glyphName") def drop(self, amount, edge): amb = self.ambit(tx=1, ty=1).drop(amount, edge) return self.intersection(P(amb)) def take(self, amount, edge): amb = self.ambit(tx=1, ty=1).take(amount, edge) return self.intersection(P(amb)) def inset(self, ax, ay): amb = self.ambit(tx=1, ty=1).inset(ax, ay) return self.intersection(P(amb)) @staticmethod def Enumerate(enumerable, enumerator): return P().enumerate(enumerable, enumerator) def addFrame(self, frame): return self.data(frame=frame) def xAlignToFrame(self): return self.align(self.data("frame"), y=None) def pvl(self): for idx, (_, pts) in enumerate(self.v.value): if len(pts) > 0: self.v.value[idx] = list(self.v.value[idx]) self.v.value[idx][-1] = [Point(p) for p in self.v.value[idx][-1]] return self def dots(self, radius=4, square=False): """(Necessary?) Create circles at moveTo commands""" dp = type(self)() for t, pts in self.v.value: if t == "moveTo": x, y = pts[0] if square: dp.rect(Rect((x-radius, y-radius, radius, radius))) else: dp.oval(Rect((x-radius, y-radius, radius, radius))) self.v.value = dp.v.value return self def _normPointSplat(self, p): if isinstance(p[0], Point): return p[0].xy() elif len(p) == 1: return p[0] else: return p def moveTo(self, *p) -> "P": p = self._normPointSplat(p) self._val.moveTo(p) return self def m(self, *p) -> "P": return self.moveTo(*p) def lineTo(self, *p) -> "P": p = self._normPointSplat(p) if len(self._val.value) == 0: self._val.moveTo(p) else: self._val.lineTo(p) return self def l(self, *p) -> "P": return self.lineTo(*p) def qCurveTo(self, *points) -> "P": self._val.qCurveTo(*points) return self def q(self, *p) -> "P": return self.qCurveTo(*p) def curveTo(self, *points) -> "P": self._val.curveTo(*points) return self def c(self, *p) -> "P": return self.curveTo(*p) def closePath(self): self._val.closePath() return self def cp(self): return self.closePath() def endPath(self): self._val.endPath() return self def ep(self): return self.endPath() def addComponent(self, baseGlyphName, transformation) -> "P": print("pen.addComponent('%s', %s)" % (baseGlyphName, tuple(transformation))) return self def points(self, pts, close=True) -> "P": self.moveTo(pts[0]) for p in pts[1:]: self.lineTo(p) if close: self.closePath() else: self.endPath() return self def point_list(self, random_seed=None): all_pts = [] for idx, (mv, pts) in enumerate(self._val.value): all_pts.extend([Point(*p) for p in pts]) if random_seed is not None: from random import Random rnd = Random() rnd.seed(random_seed) rnd.shuffle(all_pts) return all_pts def replay(self, pen) -> "P": self._val.replay(pen) for el in self._els: el.replay(pen) return self def record(self, pen) -> "P": """Play a pen into this pen, meaning that pen will be added to this one’s value.""" if hasattr(pen, "value"): pen.replay(self._val) return self if len(pen) > 0: for el in pen._els: self.record(el._val) elif pen: if isinstance(pen, Path): self.withJSONValue(pen) else: pen.replay(self._val) return self def unended(self): if not self.val_present(): return None if len(self._val.value) == 0: return True elif self._val.value[-1][0] not in ["endPath", "closePath"]: return True return False def fully_close_path(self): if not self.val_present(): # TODO log noop? return self if self._val.value[-1][0] == "closePath": start = self._val.value[0][-1][-1] end = self._val.value[-2][-1][-1] if start != end: self._val.value = self._val.value[:-1] self.lineTo(start) self.closePath() return self fullyClosePath = fully_close_path def rect(self, rect) -> "P": """Rectangle primitive — `moveTo/lineTo/lineTo/lineTo/closePath`""" rect = Rect(rect) self.moveTo(rect.point("SW").xy()) self.lineTo(rect.point("SE").xy()) self.lineTo(rect.point("NE").xy()) self.lineTo(rect.point("NW").xy()) self.closePath() return self r = rect def roundedRect(self, rect, hr, vr=None, scale=True) -> "P": """Rounded rectangle primitive""" if vr is None: vr = hr l, b, w, h = Rect(rect) r, t = l + w, b + h K = 4 * (math.sqrt(2)-1) / 3 if scale: circle = hr == 0.5 and vr == 0.5 if hr <= 0.5: hr = w * hr if vr <= 0.5: vr = h * vr else: circle = False self.moveTo((l + hr, b)) if not circle: self.lineTo((r - hr, b)) self.curveTo((r+hr*(K-1), b), (r, b+vr*(1-K)), (r, b+vr)) if not circle: self.lineTo((r, t-vr)) self.curveTo((r, t-vr*(1-K)), (r-hr*(1-K), t), (r-hr, t)) if not circle: self.lineTo((l+hr, t)) self.curveTo((l+hr*(1-K), t), (l, t-vr*(1-K)), (l, t-vr)) if not circle: self.lineTo((l, b+vr)) self.curveTo((l, b+vr*(1-K)), (l+hr*(1-K), b), (l+hr, b)) self.closePath() return self rr = roundedRect def oval(self, rect) -> "P": """Oval primitive""" if isinstance(rect, Point): self.roundedRect(Rect.FromCenter(rect, 20, 20), 0.5, 0.5) else: self.roundedRect(rect, 0.5, 0.5) return self o = oval def superellipse(self, r, factor=65): return (self .moveTo(r.pw) .bxc(r.ps, "SW", factor) .bxc(r.pe, "SE", factor) .bxc(r.pn, "NE", factor) .bxc(r.pw, "NW", factor)) def line(self, points, moveTo=True, endPath=True) -> "P": """Syntactic sugar for `moveTo`+`lineTo`(...)+`endPath`; can have any number of points""" if isinstance(points, Line): points = list(points) if len(points) == 0: return self if len(self._val.value) == 0 or moveTo: self.moveTo(points[0]) else: self.lineTo(points[0]) for p in points[1:]: self.lineTo(p) if endPath: self.endPath() return self def hull(self, points) -> "P": """Same as `.line` but calls closePath instead of endPath`""" self.moveTo(points[0]) for pt in points[1:]: self.lineTo(pt) self.closePath() return self def round(self): """Round the values of this pen to integer values.""" return self.round_to(1) def round_to(self, rounding) -> "P": """Round the values of this pen to nearest multiple of rounding.""" def rt(v, mult): rndd = float(round(v / mult) * mult) if rndd.is_integer(): return int(rndd) else: return rndd rounded = [] for t, pts in self._val.value: _rounded = [] for p in pts: if p: x, y = p _rounded.append((rt(x, rounding), rt(y, rounding))) else: _rounded.append(p) rounded.append((t, _rounded)) self._val.value = rounded return self # Compound curve mechanics def interpCurveTo(self, p1, f1, p2, f2, to, inset=0) -> "P": a = Point(self._val.value[-1][-1][-1]) d = Point(to) pl = Line(p1, p2).inset(inset) b = Line(a, pl.start).t(f1/100) c = Line(d, pl.end).t(f2/100) return self.curveTo(b, c, d) def ioc(self, pt, slope=0, fA=0, fB=85) -> "P": return self.ioEaseCurveTo(pt, slope, fA, fB) def ioEaseCurveTo(self, pt, slope=0, fA=0, fB=85) -> "P": a = Point(self._val.value[-1][-1][-1]) d = Point(pt) box = Rect.FromMnMnMxMx([ min(a.x, d.x), min(a.y, d.y), max(a.x, d.x), max(a.y, d.y) ]) if a.y < d.y: line_vertical = Line(box.ps, box.pn) else: line_vertical = Line(box.pn, box.ps) angle = Line(a, d).angle() - line_vertical.angle() try: fA1, fA2 = fA except TypeError: fA1, fA2 = fA, fA try: fB1, fB2 = fB except TypeError: fB1, fB2 = fB, fB rotated = line_vertical.rotate(math.degrees(angle*(slope/100))) vertical = Line(rotated.intersection(box.es), rotated.intersection(box.en)) if a.y > d.y: vertical = vertical.reverse() c1 = Line(a, vertical.start).t(fA1) c2 = Line(vertical.mid, vertical.start).t(fA1) self.lineTo(c1) self.curveTo( Line(c1, vertical.start).t(fB1), Line(c2, vertical.start).t(fB1), c2) c1 = Line(vertical.mid, vertical.end).t(fA2) c2 = Line(d, vertical.end).t(fA2) self.lineTo(c1) self.curveTo( Line(c1, vertical.end).t(fB2), Line(c2, vertical.end).t(fB2), c2) self.lineTo(d) return self def bxc(self, pt, point, factor=0.65, po=(0, 0), mods={}, flatten=False): return self.boxCurveTo(pt, point, factor, po, mods, flatten) def roundedCorner(self, pt, point, multipliers, offset=4, factor=65) -> "P": a, b, c, d = multipliers return (self .lineTo(pt.offset(offset*a, offset*b)) .boxCurveTo(pt.offset(offset*c, offset*d), point, factor=factor)) def boxCurveTo(self, pt, point, factor=0.65, po=(0, 0), mods={}, flatten=False): if flatten: self.lineTo(pt) return self a = Point(self._val.value[-1][-1][-1]) d = Point(pt) box = Rect.FromMnMnMxMx([ min(a.x, d.x), min(a.y, d.y), max(a.x, d.x), max(a.y, d.y) ]) try: f1, f2 = factor except TypeError: if isinstance(factor, Atom): f1, f2 = (factor[0], factor[0]) else: f1, f2 = (factor, factor) if isinstance(point, str): #print("POINT", point) if point == "cx": # ease-in-out if a.y < d.y: p1 = box.pse p2 = box.pnw elif a.y > d.y: p1 = box.pne p2 = box.psw else: p1 = p2 = a.interp(0.5, d) elif point == "e": # ease-in if a.y < d.y: p1 = p2 = box.pse elif a.y > d.y: p1 = p2 = box.pne else: p1 = p2 = a.interp(0.5, d) elif point == "w": # ease-out if a.y < d.y: p1 = p2 = box.pnw elif a.y > d.y: p1 = p2 = box.psw else: p1 = p2 = a.interp(0.5, d) else: if "," in point: pt1, pt2 = [x.strip() for x in point.split(",")] p1 = box.point(pt1) p2 = box.point(pt2) else: p = box.point(point) p1, p2 = (p, p) elif isinstance(point, Point): p1, p2 = point, point else: p1, p2 = point p1 = box.point(p1) p2 = box.point(p2) p1 = p1.offset(*po) p2 = p2.offset(*po) b = a.interp(f1, p1) c = d.interp(f2, p2) mb = mods.get("b") mc = mods.get("c") if mb: b = mb(b) elif mc: c = mc(c) self.curveTo(b, c, d) return self def mirror(self, factors, point=None): if point == 0: point = (0, 0) return (self.layer(1, lambda p: p.scale(*factors, point=point or self.ambit().psw))) def mirrorx(self, point=None) -> "P": return self.mirror((-1, 1), point=point) def mirrory(self, point=None) -> "P": return self.mirror((1, -1), point=point) def mirrorxy(self, point=None) -> "P": return self.mirror((-1, -1), point=point) def pattern(self, rect, clip=False) -> "P": dp_copy = self.copy() #dp_copy.value = self.value for y in range(-1, 1): for x in range(-1, 1): dpp = type(self)() dp_copy.replay(dpp) dpp.translate(rect.w*x, rect.h*y) dpp.replay(self) self.translate(rect.w/2, rect.h/2) if clip: clip_box = type(self)().rect(rect) return self.intersection(clip_box) return self def withRect(self, rect, fn:Callable[[Rect, "P"], "P"]) -> "P": r = Rect(rect) return fn(r, self).data(frame=r) def gridlines(self, rect, x=20, y=None, absolute=False) -> "P": """Construct a grid in the pen using `x` and (optionally) `y` subdivisions""" xarg = x yarg = y or x if absolute: x = int(rect.w / xarg) y = int(rect.h / yarg) else: x = xarg y = yarg for _x in rect.subdivide(x, "minx"): if _x.x > 0 and _x.x > rect.x: self.line([_x.point("NW"), _x.point("SW")]) for _y in rect.subdivide(y, "miny"): if _y.y > 0 and _y.y > rect.y: self.line([_y.point("SW"), _y.point("SE")]) return self.f(None).s(0, 0.1).sw(3) def ez(self, r, start_y, end_y, s) -> "P": self.moveTo(r.edge("W").t(start_y)) self.gs(s, do_close=False, first_move="lineTo") self.lineTo(r.edge("E").t(end_y)) self.endPath() return self def segments(self, all_curves=False) -> "P": if not self.val_present(): for idx, el in enumerate(self._els): self._els[idx] = el.segments() return self segs = [] last = None for contour in self.copy().explode(): for mv, pts in contour.v.value: if last: if mv == "curveTo": segs.append(type(self)().moveTo(last).curveTo(*pts)) if mv == "lineTo": if all_curves: ln = Line(last, pts[0]) segs.append(type(self)().moveTo(ln.start).curveTo(ln.t(0.25), ln.t(0.75), ln.end)) else: segs.append(type(self)().moveTo(last).lineTo(*pts)) if len(pts) > 0: last = pts[-1] else: last = None self._val = None self._els = segs return self def join(self): self._val = RecordingPen() self._val.moveTo(self._els[0].v.value[0][-1][-1]) for el in self._els: self._val.value.extend(el.v.value[1:]) self._els = [] return self def substructure(self): indicators = type(self)() def append(p): substructure = p.data("substructure") if substructure: indicators.append(substructure) self.mapv(append) return indicators def bounds(self): """Calculate the exact bounds of this shape, using a BoundPen""" b = Rect(0, 0, 0, 0) if self.val_present(): try: cbp = BoundsPen(None) self._val.replay(cbp) mnx, mny, mxx, mxy = cbp.bounds b = Rect((mnx, mny, mxx - mnx, mxy - mny)) except: pass if len(self._els) > 0: bs = [] for el in self._els: eb = el.bounds() if eb and eb.nonzero(): bs.append(eb) if len(bs) > 0: b = bs[0] for eb in bs[1:]: b = b.union(eb) return b def _normT(self, th, tv, tx, ty, t) -> "P": import traceback global THTV_WARNING if th is not None: #traceback.print_stack() tx = th if not THTV_WARNING: print("! API CHANGE: th/tv are now tx/ty !") THTV_WARNING = True if tv is not None: #traceback.print_stack() ty = tv if not THTV_WARNING: print("! API CHANGE: th/tv are now tx/ty !") THTV_WARNING = True if t is not None: tx = bool(int(t)) if tx: ty = int((t-1)*10) == 1 else: ty = int(t)*10 == 1 else: tx, ty = tx, ty return tx, ty def empty(self): return len(self._val.value) == 0 def ambit(self, th=None, tv=None, tx=0, ty=0, t=None) -> Rect: """ Get the calculated rect boundary. - `tx` means `(t)rue (x)` (i.e. the true width/horizontal dimension (was previously th)); - `ty` means `(t)rue (y)` (i.e. the true height/vertical dimension (was previously tv)); Passing either ignores a non-bounds-derived frame in either dimension """ tx, ty = self._normT(th, tv, tx, ty, t) f = self._data.get("frame", None) # true bounds if tx and ty: return self.bounds() # true no-bounds elif not tx and not ty and f: return f # partial bounds elif f and (self.val_present() or (self.data("glyphName") and len(self) == 0)): if self.empty(): if tx: f = f.setw(0) elif ty: f = f.seth(0) return f else: b = self.bounds() if tx: return Rect(b.x, f.y, b.w, f.h) else: return Rect(f.x, b.y, f.w, b.h) # pass-to-els elif len(self._els) > 0: try: union = self._els[0].ambit(tx=tx, ty=ty) for p in self._els[1:]: a = p.ambit(tx=tx, ty=ty) if a.x == 0 and a.y == 0 and a.w == 0 and a.h == 0: continue union = union.union(a) return union except Exception as _: return Rect(0,0,0,0) # catch-all return self.bounds() # if f or self._val: # if (th or tv) and not self.empty(): # b = self.bounds() # if th and tv: # return b # elif th: # return Rect(b.x, f.y, b.w, f.h) # else: # return Rect(f.x, b.y, f.w, b.h) # else: # if self.empty(): # if th: # f = f.setw(0) # elif tv: # f = f.seth(0) # return f # return f # elif : # return self.bounds() getFrame = ambit def align(self, rect, x="mdx", y="mdy", th=None, # deprecated tv=None, # deprecated tx=1, ty=0, transformFrame=True, h=None, returnOffset=False ) -> "P": """ Align this pen to another rect, defaults to the center. - `tx` means true-x (i.e. will disregard any invisible 'frame' set on the pen (as in the case of glyphs returned from StSt/Glyphwise)); - `ty` means true-y, which is the same but for the vertical dimension """ if not isinstance(rect, Rect): if hasattr(rect, "ambit"): rect = rect.ambit(tx=tx, ty=ty) elif isinstance(rect, Point) and rect._rect is not None: x, y = rect._corner rect = rect._rect elif hasattr(rect, "rect"): rect = rect.rect else: raise Exception("can't align to this object") tx, ty = self._normT(th, tv, tx, ty, None) r = self.ambit(tx=tx, ty=ty) if h is not None: r = r.seth(h) self.data(_last_align_rect=rect) offset = align(r, rect, x, y) self.translate(*offset, transformFrame=transformFrame) if returnOffset: return offset else: return self def _align_compass(self, compass, rect, tx=1, ty=0) -> "P": return self.align(rect, compass, tx=tx, ty=ty) #å = align alne = partialmethod(_align_compass, "NE") ale = partialmethod(_align_compass, "E") alse = partialmethod(_align_compass, "SE") als = partialmethod(_align_compass, "S") alsw = partialmethod(_align_compass, "SW") alw = partialmethod(_align_compass, "W") alnw = partialmethod(_align_compass, "NW") aln = partialmethod(_align_compass, "N") def xalign(self, rect=None, x="centerx", th=None, tv=None, tx=1, ty=0) -> "P": tx, ty = self._normT(th, tv, tx, ty, None) if x == "C": x = "CX" if rect is None: rect = self.ambit(tx=tx, ty=ty) if callable(rect): rect = rect(self) self.align(rect, x=x, y=None, tx=tx, ty=ty) for el in self._els: el.align(rect, x=x, y=None, tx=tx, ty=ty) return self #xå = xalign def yalign(self, rect=None, y="centery", th=None, tv=None, tx=0, ty=1) -> "P": tx, ty = self._normT(th, tv, tx, ty, None) if rect is None: rect = self.ambit(tx=tx, ty=ty) if callable(rect): rect = rect(self) self.align(rect, x=None, y=y, tx=tx, ty=ty) return self #yå = yalign def _normPoint(self, point=None, th=None, tv=None, tx=0, ty=0, **kwargs) -> "P": tx, ty = self._normT(th, tv, tx, ty, kwargs.get("t")) if "pt" in kwargs: point = kwargs["pt"] a = self.ambit(tx=tx, ty=ty) if point is None: return a.pc elif point == 0: return a.psw elif point is False: return Point(0, 0) elif isinstance(point, str): if point.startswith("tx"): a = self.ambit(tx=1, ty=0) point = point[2:] elif point.startswith("ty"): a = self.ambit(tx=0, ty=1) point = point[2:] elif point.startswith("t"): a = self.ambit(tx=1, ty=1) point = point[1:] return a.point(point) elif (not (isinstance(point[1], int) or isinstance(point[1], float)) and hasattr(self, "_normPoint")): return self[point[0]]._normPoint(point[1]) else: return Point(point) def transform(self, transform, transformFrame=True) -> "P": """Perform an arbitrary transformation on the pen, using the fontTools `Transform` class.""" if self.val_present(): op = RecordingPen() tp = TransformPen(op, transform) self._val.replay(tp) self._val.value = op.value f = self._data.get("frame") if transformFrame and f: self.data(frame=f.transform(transform)) for p in self._els: p.transform(transform, transformFrame=transformFrame) substructure = self._data.get("substructure") if substructure: substructure.transform(transform, transformFrame=transformFrame) img = self.img() if img: img["rect"] = img["rect"].transform(transform) return self def matrix(self, a, b, c, d, e, f, transformFrame=False) -> "P": return self.transform(Transform(a, b, c, d, e, f), transformFrame=transformFrame) def invertYAxis(self, height) -> "P": rp = RecordingPen() tp = TransformPen(rp, (1, 0, 0, -1, 0, height)) self.replay(tp) self._val.value = rp.value return self def nonlinear_transform(self, fn) -> "P": for el in self._els: el.nonlinear_transform(fn) if self.val_present(): for idx, (move, pts) in enumerate(self._val.value): if len(pts) > 0: _pts = [] for _pt in pts: x, y = _pt _pts.append(fn(x, y)) self._val.value[idx] = (move, _pts) return self nlt = nonlinear_transform def translate(self, x, y=None, transformFrame=True) -> "P": """Translate this shape by `x` and `y` (pixel values).""" if y is None: y = x return self.transform(Transform(1, 0, 0, 1, x, y), transformFrame=transformFrame) offset = translate t = translate def shift(self, dx, dy, tx=1, ty=1) -> "P": amb = self.ambit(tx=tx, ty=ty) self.translate(amb.w*dx, amb.h*dy) return self sh = shift def zero(self, th=None, tv=None, tx=0, ty=0) -> "P": tx, ty = self._normT(th, tv, tx, ty, None) x, y, _, _ = self.ambit(tx=tx, ty=ty) self.translate(-x, -y) return self def centerZero(self, th=None, tv=None, tx=0, ty=0): tx, ty = self._normT(th, tv, tx, ty, None) x, y, w, h = self.ambit(tx=tx, ty=ty) nx, ny = -x-w/2, -y-h/2 return (self .t(-x-w/2, -y-h/2) .data(centerZeroOffset=(nx, ny))) def centerPoint(self, rect, pt, interp=1, th=None, tv=None, tx=1, ty=0, **kwargs) -> "P": tx, ty = self._normT(th, tv, tx, ty, None) if "i" in kwargs: interp = kwargs["i"] x, y = self._normPoint(pt, tx=tx, ty=ty, **kwargs) return self.translate(norm(interp, 0, rect.w/2-x), norm(interp, 0, rect.h/2-y)) def skew(self, x=0, y=0, point=None, th=None, tv=None, tx=1, ty=0, **kwargs) -> "P": tx, ty = self._normT(th, tv, tx, ty, None) t = Transform() px, py = self._normPoint(point, tx=tx, ty=ty, **kwargs) t = t.translate(px, py) t = t.skew(x, y) t = t.translate(-px, -py) return self.transform(t) def rotate(self, degrees, point=None, th=None, tv=None, tx=1, ty=1, **kwargs) -> "P": """Rotate this shape by a degree (in 360-scale, counterclockwise).""" tx, ty = self._normT(th, tv, tx, ty, None) t = Transform() x, y = self._normPoint(point, tx=tx, ty=ty, **kwargs) t = t.translate(x, y) t = t.rotate(math.radians(degrees)) t = t.translate(-x, -y) return self.transform(t, transformFrame=False) rt = rotate def r90(self, multiplier, point=None, tx=1, ty=1, **kwargs) -> "P": return self.rotate(90*multiplier, point=point, tx=tx, ty=ty, **kwargs) def scale(self, scaleX, scaleY=None, point=None, th=None, tv=None, tx=1, ty=0, **kwargs) -> "P": """Scale this shape by a percentage amount (1-scale).""" tx, ty = self._normT(th, tv, tx, ty, None) t = Transform() x, y = self._normPoint(point, tx=tx, ty=ty, **kwargs) if point is not False: t = t.translate(x, y) t = t.scale(scaleX, scaleY or scaleX) if point is not False: t = t.translate(-x, -y) return self.transform(t) def flipx(self): return self.scale(-1,1) def flipy(self): return self.scale(1,-1) def scaleToRect(self, rect, preserveAspect=True, shrink_only=False, tx=1, ty=0, return_number=False) -> "P": """Scale this shape into a `Rect`.""" bounds = self.bounds() if not bounds.nonzero(): return self h = rect.w / bounds.w v = rect.h / bounds.h if preserveAspect: scale = h if h < v else v if shrink_only and scale >= 1: if return_number: return 1 return self if return_number: return scale else: return self.scale(scale, tx=tx, ty=ty) else: if shrink_only and (h >= 1 or v >= 1): if return_number: return 1, 1 return self if return_number: return h, v return self.scale(h, v, tx=tx, ty=ty) def scaleToWidth(self, w, shrink_only=False) -> "P": """Scale this shape horizontally""" b = self.bounds() if shrink_only and b.w < w: return self else: return self.scale(w / self.bounds().w, 1) def scaleToHeight(self, h, shrink_only=False) -> "P": """Scale this shape horizontally""" b = self.bounds() if shrink_only and b.h < h: return self return self.scale(1, h / self.bounds().h) # multi-elements def distribute(self, v=False, tracks=None, th=None, tv=None, tx=0, ty=0) -> "P": tx, ty = self._normT(th, tv, tx, ty, None) off = 0 for idx, p in enumerate(self): if tracks is not None and idx > 0: t = tracks[idx-1] #print(t) off += t frame = p.ambit(tx=tx, ty=ty) if v: if frame.y < 0: p.translate(0, -frame.y) p.translate(0, off) off += frame.h else: if frame.x < 0: p.translate(-frame.x, 0) if frame.x > 0 and th: p.translate(-frame.x, 0) p.translate(off, 0) off += frame.w return self def spread(self, tracking=0, tx=0, zero=False) -> "P": "Horizontal distribution of elements" if zero: for p in self: p.zero(tx=tx) ambits = [p.ambit(tx=tx, ty=0).expand(tracking, "E") for p in self._els] ax = 0 for idx, p in enumerate(self._els): aw = ambits[idx].w p.translate(ax, 0) ax += aw return self def stack(self, leading=0, ty=0, zero=False) -> "P": "Vertical distribution of elements" if isinstance(leading, str) and "%" in leading: leading = self[0].ambit(ty=0).h * float(leading[:-1])/100 if zero: for p in self: p.zero() ambits = [p.ambit(tx=0, ty=ty).expand(leading, "N") for p in self._els] for idx, p in enumerate(self._els): for a in ambits[idx+1:]: p.translate(0, a.h) return self def track(self, t, v=False) -> "P": """Track-out/distribute elements""" for idx, p in enumerate(self._els): if v: p.translate(0, -t*idx) else: p.translate(t*idx, 0) return self def lead(self, leading) -> "P": "Vertical spacing" ln = len(self._els) try: if self._els[-1].ambit().y > self._els[0].ambit().y: leading = -leading except IndexError: pass for idx, p in enumerate(self._els): p.translate(0, leading*(ln-1-idx)) return self def grid(self, every, spread=0, stack=0, zero=False) -> "P": top = type(self)() row = None for idx, p in enumerate(self._els): if zero: p.zero() if idx%every == 0: row = type(self)() top.append(row) row.append(p) self._els = top._els for row in self: row.spread(spread) self.stack(stack) return self def gridlayer(self, nx, ny=None, track=0, lead=0) -> "P": """Spread nx copies and then stack ny copies, w/ optional tracking & leading""" return (self .layer(nx) .spread(track) .layer(ny if ny is not None else nx) .stack(lead)) def pasteup(self, styler=lambda p: p.f(bw(1)), padding=(5, 5), tx=1, ty=0, x="CX", y="CY"): r = self.ambit(tx=tx, ty=ty).inset(*[-x for x in padding]).zero() board = type(self)(r).ch(styler) self.align(r, tx=tx, ty=ty, x=x, y=y) return self.up().insert(0, board) def pattern_repeat(self, r) -> "P": a = self.ambit(tx=1, ty=1) copies = type(self)() def repeater(_p): if a.mxx > r.mxx: copies.append(_p.copy().translate(-r.w, 0).fssw(hsl(0, a=0.5), -1, 0).rotate(0)) if a.mxy > r.mxy: copies.append(_p.copy().translate(0, -r.h).fssw(hsl(0.25, a=0.5), -1, 0).rotate(0)) if a.mny < r.mny: copies.append(_p.copy().translate(0, r.h).fssw(hsl(0.5, a=0.5), -1, 0).rotate(0)) if a.mnx < r.mnx: copies.append(_p.copy().translate(r.w, 0).fssw(hsl(0.75, a=0.5), -1, 0).rotate(0)) repeater(self) for c in list(copies): repeater(c) return self.up().append(copies) def track_with_width(self, t) -> "P": """Track-out/distribute elements""" x = 0 for idx, p in enumerate(self._els): frame = p.ambit() p.translate(x + t, 0) x += frame.w return self def track_to_width(self, width, pullToEdges=False, r=0) -> "P": return self.track_to_rect(Rect(width, 0), pullToEdges=pullToEdges, r=r) def track_to_rect(self, rect, pullToEdges=False, r=0) -> "P": """Distribute pens evenly within a frame""" if len(self) == 1: return self.align(rect) total_width = 0 pens = self._els if r: pens = list(reversed(pens)) start_x = pens[0].ambit(tx=pullToEdges).x end_x = pens[-1].ambit(tx=pullToEdges).point("SE").x # TODO easy to knock out apostrophes here based on a callback, last "actual" frame total_width = end_x - start_x leftover_w = rect.w - total_width tracking_value = leftover_w / (len(self)-1) if pullToEdges: xoffset = rect.x - pens[0].bounds().x else: xoffset = rect.x - pens[0].ambit().x for idx, p in enumerate(pens): if idx == 0: p.translate(xoffset, 0) else: p.translate(xoffset+tracking_value*idx, 0) return self trackToRect = track_to_rect def connect(self, *others) -> "P": return (type(self)([self, *others]) .distribute() .pen()) @property def x(self): return self.ambit().x @property def y(self): return self.ambit().y @property def w(self): return self.ambit().w @property def h(self): return self.ambit().h @property def tx(self): return self.ambit(tx=1).x @property def ty(self): return self.ambit(ty=1).y @property def tw(self): return self.ambit(tx=1).w @property def th(self): return self.ambit(ty=1).h def groupedStyle(self, st, default_style): sf = False if "stroke" in st: c = st["stroke"] sw = st.get("strokeWidth", default_style.get("strokeWidth", 1)) miter = st.get("strokeMiter", default_style.get("strokeMiter", None)) st["stroke"] = dict(color=c, weight=sw, miter=miter) if "stroke" not in st and "stroke" in default_style: st["stroke"] = self.groupedStyle(default_style, default_style)["stroke"] if "strokeWidth" in st: del st["strokeWidth"] if "strokeMiter" in st: del st["strokeMiter"] if "strokeFirst" in st: sf = True del st["strokeFirst"] if "fill" not in st: st["fill"] = rgb(1, 0, 0.5) rest = ["blendmode", "image", "skp", "COLR", "dash"] if sf: order = ["shadow", "stroke", "fill", *rest] else: order = ["shadow", "fill", "stroke", *rest] sort = {k:v for k,v in sorted(st.items(), key=lambda kv: order.index(kv[0]))} return sort def f(self, *value) -> "P": """Get/set a (f)ill""" if value: if isinstance(value[0], Theme): for k, v in value[0].colors.items(): self.attr(k, fill=v) return self elif not isinstance(value, Color): value = normalize_color(value) return self.attr(fill=value) else: return self.attr(field="fill") fill = f def s(self, *value) -> "P": """Get/set a (s)troke""" if value: if isinstance(value[0], Theme): for k, v in value[0].colors.items(): self.attr(k, stroke=v) return self elif not isinstance(value, Color): value = normalize_color(value) return self.attr(stroke=value) else: return self.attr(field="stroke") stroke = s def sw(self, value) -> "P": """Get/set a (s)troke (w)idth""" if value is not None: return self.attr(strokeWidth=value) else: return self.attr(field="strokeWidth") strokeWidth = sw def dash(self, pattern, phase=0) -> "P": return self.attr(dash=[pattern, phase]) def ssw(self, s, sw) -> "P": self.s(s) self.sw(sw) return self def fssw(self, f, s, sw, sf=0) -> "P": if not isinstance(f, Theme) and isinstance(s, Theme): t = Theme(f) for k in s.colors.keys(): t[k] = f f = t self.f(f) self.s(s) self.sw(sw) self.sf(sf) return self def strokeFirst(self, value=None) -> "P": """ For a rendering engine that has to stroke and fill in two separate passes, perform the stroke _before_ the fill (akin to an `.understroke` but without the duplication overhead) """ if value: return self.attr(strokeFirst=value) else: return self.attr(field="strokeFirst") def sf(self, value=None) -> "P": "strokeFirst" return self.strokeFirst(value) def strokeMiter(self, value=None) -> "P": """ For a rendering engine that can specify stroke-miter """ if value: return self.attr(strokeMiter=value) else: return self.attr(field="strokeMiter") def sm(self, value=None) -> "P": "strokeMiter" return self.strokeMiter(value) def img(self, src=None, rect=Rect(0, 0, 500, 500), pattern=False, opacity=1.0): """Get/set an image fill""" if src: from coldtype.img.abstract import AbstractImage if isinstance(src, AbstractImage): return self.attr(image=dict(src=src.src, rect=rect, pattern=pattern, opacity=opacity)) return self.attr(image=dict(src=src, rect=rect, pattern=pattern, opacity=opacity)) else: return self.attr(field="image") image = img def shadow(self, radius=10, color=(0, 0.3), clip=None): return self.attr(shadow=dict(color=normalize_color(color), radius=radius, clip=clip)) # other def blendmode(self, blendmode=None, show=False) -> "P": if isinstance(blendmode, int): blendmode = BlendMode.Cycle(blendmode, show=show) elif isinstance(blendmode, str): blendmode = BlendMode[blendmode] if blendmode: return self.attr(blendmode=blendmode) else: return self.attr(field="blendmode") def postprocess(self, fn) -> "P": return self.data(postprocess=fn, function_literals=True) def glyph(self, glyph, glyphSet=None, layerComponents=False) -> "P": """Play a glyph (like from `defcon`) into this pen.""" out = type(self)() base = type(self)() out.append(base) glyph.draw(base._val) new_val = [] for mv, pts in base._val.value: if mv == "addComponent": component_name, matrix = pts rp = RecordingPen() tp = TransformPen(rp, Transform(*matrix)) component = glyphSet[component_name] # recursively realize any nested components realized = type(self)().glyph(component, glyphSet) realized.replay(tp) p = type(self)() p._val = rp out.append(p) if "addComponent" in str(p._val.value): print("> NESTED COMPONENT", component_name) else: new_val.append((mv, pts)) base._val.value = new_val if layerComponents: return out else: try: out.pen().replay(self._val) except IndexError: pass return self def toGlyph(self, name=None, width=None, allow_blank=False): """ Create a glyph (like from `defcon`) using this pen’s value. *Warning*: if path is unended, closedPath will be called """ from defcon import Glyph if not allow_blank: if self.unended(): self.closePath() bounds = self.bounds() glyph = Glyph() glyph.name = name glyph.width = width or bounds.w try: sp = glyph.getPen() self.replay(sp) except AssertionError: if not allow_blank: print(">>>blank glyph:", glyph.name) return glyph def pickle(self, dst) -> "P": dst.parent.mkdir(parents=True, exist_ok=True) fh = open(str(dst), "wb") def prune(pen, state, data): if state >= 0: if hasattr(pen, "_stst"): pen._stst = None self.walk(prune) pickle.dump(self, fh) fh.close() return self def Unpickle(self, src): if isinstance(src, str): src = Path(src) return pickle.load(open(str(src.expanduser()), "rb")) def withJSONValue(self, path, keys=None) -> "P": data = json.loads(Path(path).expanduser().read_text()) if keys is not None: for key in keys: data = data[key] self._val.value = data return self def withSVG(self, svg) -> "P": from fontTools.svgLib import SVGPath svg = SVGPath.fromstring(svg) rp = RecordingPen() svg.draw(rp) self._val.value = rp.value return self def withSVGFile(self, svg_file) -> "P": svg_file = Path(svg_file).expanduser().absolute() from fontTools.svgLib import SVGPath svg = SVGPath.fromstring(svg_file.read_bytes()) rp = RecordingPen() svg.draw(rp) self._val.value = rp.value return self def _pathop(self, otherPen=None, operation=BooleanOp.XOR, use_skia_pathops_draw=True) -> "P": if callable(otherPen): otherPen = otherPen(self) if self.val_present(): self._val.value = calculate_pathop(self, otherPen, operation, use_skia_pathops_draw=use_skia_pathops_draw) if otherPen is not None or operation == BooleanOp.Simplify: for el in self._els: el._pathop(otherPen, operation) else: curr = self._els[0] for el in self._els[1:]: curr._pathop(el, operation) self._els = [curr] # if hasattr(self, "pmap"): # return self.pmap(lambda p: p._pathop(otherPen, operation)) # self.value = calculate_pathop(self, otherPen, operation) return self def difference(self, otherPen=None) -> "P": """Calculate and return the difference of this shape and another.""" return self._pathop(otherPen=otherPen, operation=BooleanOp.Difference) def union(self, otherPen=None) -> "P": """Calculate and return the union of this shape and another.""" return self._pathop(otherPen=otherPen, operation=BooleanOp.Union) def xor(self, otherPen=None) -> "P": """Calculate and return the XOR of this shape and another.""" return self._pathop(otherPen=otherPen, operation=BooleanOp.XOR) def reverseDifference(self, otherPen=None) -> "P": """Calculate and return the reverseDifference of this shape and another.""" return self._pathop(otherPen=otherPen, operation=BooleanOp.ReverseDifference) def intersection(self, otherPen=None) -> "P": """Calculate and return the intersection of this shape and another.""" return self._pathop(otherPen=otherPen, operation=BooleanOp.Intersection) def removeOverlap(self, use_skia_pathops_draw=True) -> "P": """Remove overlaps within this shape and return itself.""" return self._pathop(otherPen=None, operation=BooleanOp.Simplify, use_skia_pathops_draw=use_skia_pathops_draw) remove_overlap = removeOverlap ro = removeOverlap def distribute_on_path(self, path, offset=0, cc=None, notfound=None, center=False, apply_tangent=True, baseline=0, ) -> "P": if len(self) == 0: # TODO print error? return self if cc: cutter = cc else: cutter = CurveCutter(path) if center is not False: offset = (cutter.length-self.bounds().w)/2 + center limit = len(self._els) for idx, p in enumerate(self._els): f = p.ambit() bs = f.y ow = offset + f.x + f.w / 2 #if ow < 0: # if notfound: # notfound(p) if ow > cutter.length: limit = min(idx, limit) else: _p, tangent = cutter.subsegmentPoint(end=ow) x_shift = bs * math.cos(math.radians(tangent)) y_shift = bs * math.sin(math.radians(tangent)) if baseline > 1: p.translate(0, -f.h*baseline) t = Transform() t = t.translate(_p[0] + x_shift - f.x, _p[1] + y_shift - f.y) t = t.translate(f.x, f.y) if apply_tangent: t = t.rotate(math.radians(tangent-90)) else: p.data(tangent=tangent-90) t = t.translate(-f.x, -f.y) t = t.translate(-f.w*0.5) p.transform(t) if limit < len(self._els): self._els = self._els[0:limit] return self distributeOnPath = distribute_on_path def subsegment(self, start=0, end=1) -> "P": """Return a subsegment of the pen based on `t` values `start` and `end`""" if not self.val_present(): return cc = CurveCutter(self) start = 0 end = end * cc.calcCurveLength() pv = cc.subsegment(start, end) self._val.value = pv return self def point_t(self, t=0.5): """Get point value for time `t`""" cc = CurveCutter(self) start = 0 tv = t * cc.calcCurveLength() p, tangent = cc.subsegmentPoint(start=0, end=tv) return p, tangent def split_t(self, t=0.5): if not self.val_present(): return a = self._val.value[0][-1][0] b, c, d = self._val.value[-1][-1] return splitCubicAtT(a, b, c, d, t) def add_pt_t(self, cuidx, t) -> "P": if not self.val_present(): return cidx = 0 insert_idx = -1 c1, c2 = None, None for idx, (mv, pts) in enumerate(self._val.value): if mv == "curveTo": if cidx == cuidx: insert_idx = idx a = self._val.value[idx-1][-1][-1] b, c, d = pts c1, c2 = splitCubicAtT(a, b, c, d, t) cidx += 1 elif mv == "lineTo": if cidx == cuidx: insert_idx = idx a = self._val.value[idx-1][-1][-1] b = pts[0] l = Line(a, b) c1 = [l.t(0.5)] c2 = [b] cidx += 1 if c2: if len(c2) > 1: self._val.value[insert_idx] = ("curveTo", c1[1:]) self._val.value.insert(insert_idx+1, ("curveTo", c2[1:])) else: self._val.value[insert_idx] = ("lineTo", c1) self._val.value.insert(insert_idx+1, ("lineTo", c2)) return self def samples(self, interval=10, even=False): cc = CurveCutter(self) samples = [] length = cc.calcCurveLength() inc = 1 idx = 0 while inc < length: pt, tan = cc.subsegmentPoint(start=0, end=inc) samples.append(CurveSample(idx, pt, inc / length, tan)) inc += interval idx += 1 for i, s in enumerate(samples): next = samples[i+1] if i < len(samples)-1 else s prev = samples[i-1] if i > 0 else s s.neighbors(prev, next) return samples def onSamples(self, interval=10, even=False, fn=None): return (type(self)().enumerate(self.samples(interval=interval, even=even), lambda s: fn(self, s))) def length(self, t=1): """Get the length of the curve for time `t`""" cc = CurveCutter(self) start = 0 tv = t * cc.calcCurveLength() return tv def ease_t(self, e, tries=0) -> "P": _, _, w, h = self.ambit() pen = MarginPen(None, e*w, isHorizontal=False) self.replay(pen) try: return pen.getAll()[0]/h except IndexError: # HACK for now but I guess works? #print("INDEX ERROR", e) if tries < 500: return self.ease_t(e-0.01, tries=tries+1) return 0 def divide(self, length=150, floor=True, count=None, idx=0, max=None) -> "P": a = self.v.value[0][-1][-1] b, c, d = self.v.value[1][-1] l = calcCubicArcLength(a, b, c, d) if count is not None: length = l / count floor = False if l < length: if max is not None and len(self.v.value) < max: self.add_pt_t(0, 0.5) self.divide(length=length, floor=False, idx=idx+1, max=max) return self if max is not None and len(self.v.value) >= max: return self if floor: fl = math.floor(l/length) length = l/fl t = 1/(l/length) if l > length*1.5: self.add_pt_t(0, 1-t) self.divide(length=length, floor=False, idx=idx+1, max=max) elif max is not None: self.add_pt_t(0, 0.5) self.divide(length=length, floor=False, idx=idx+1, max=max) pass return self # contours? def _prep_for_wave(self, flatten=1, centered=False): prepped = self.copy() if centered: prepped.center(tx=1, ty=1) if flatten > 0: prepped.flatten(flatten) return prepped def toAudio(self, flatten=1, centered=False, loops=3, filename=None): import numpy as np from pedalboard.io import AudioFile prepped = self._prep_for_wave(flatten=flatten, centered=centered) left, right = [], [] for (_, pts) in prepped._val.value: if len(pts) > 0: left.append(pts[0][0] / 1000) right.append(pts[0][1] / 1000) audio = np.tile(np.array([left, right]), loops) if filename: with AudioFile(filename, "w", samplerate=48000, num_channels=2) as f: f.write(audio) return audio, len(left) def fromAudio(self, audio, start=500, end=9500, step=1, mult=1360, scale=2) -> "P": for idx in range(start, end, step): try: x = audio[0][idx] y = audio[1][idx] self.oval(Rect.FromCenter((x*mult, y*mult), scale)) except IndexError: pass return self # def wavefile(self, flatten=1, centered=False) -> str: # #from IPython.display import Audio, display # prepped = self._prep_for_wave(flatten=flatten, centered=centered) # left, right = [], [] # for (_, pts) in prepped._val.value: # if len(pts) > 0: # left.append(pts[0][0]) # right.append(pts[0][1]) # samplesPerFrame = 200 # sampleRate = 48000.0 # hertz # filename = "test_1.wav" # obj = wave.open(filename, 'w') # obj.setnchannels(2) # obj.setsampwidth(2) # obj.setframerate(sampleRate) # for x in range(0, samplesPerFrame): # for idx, l in enumerate(left): # data = struct.pack(' 0] lines = [] for i, p in enumerate(pts): if i + 1 == len(pts): lines.append(Line(p, pts[0])) else: lines.append(Line(p, pts[i+1])) mnx, mny, mxx, mxy = self.bounds().mnmnmxmx() min_ang = min([l.ang for l in lines]) max_ang = max([l.ang for l in lines]) #for idx, l in enumerate(lines): # print(idx, ">", l.ang, min_ang, max_ang) xs = [l for l in lines if l.ang < 0.25 or l.ang > 2.5] ys = [l for l in lines if 1 < l.ang < 2] if len(ys) == 2 and len(xs) < 2: xs = [l for l in lines if l not in ys] elif len(ys) < 2 and len(xs) == 2: ys = [l for l in lines if l not in xs] #for l in ys: # print(l.ang) #print(len(xs), len(ys)) #print("--------------------") try: n = [l for l in xs if l.start.y == mxy or l.end.y == mxy][0] s = [l for l in xs if l.start.y == mny or l.end.y == mny][0] e = [l for l in ys if l.start.x == mxx or l.end.x == mxx][0] w = [l for l in ys if l.start.x == mnx or l.end.x == mnx][0] return n, s, e, w except IndexError: amb = self.ambit(tx=1, ty=1) return [amb.en, amb.es, amb.ee, amb.ew] def avg(self): self.pvl() pts = [] for _, _pts in self.v.value: if len(_pts) > 0: pts.extend(_pts) n = len(pts) return Point( sum([p.x for p in pts])/n, sum([p.y for p in pts])/n) @property def ecx(self): n, s, e, w = self.nsew() return e.interp(0.5, w.reverse()) @property def ecy(self): n, s, e, w = self.nsew() return n.interp(0.5, s.reverse()) def edge(self, e) -> "P": e = e.lower() if e == "n": return self.en elif e == "s": return self.es elif e == "e": return self.ee elif e == "w": return self.ew def point(self, pt) -> "P": n, s, e, w = self.nsew() if pt == "NE": return n.pe elif pt == "NW": return n.pw elif pt == "SE": return s.pe elif pt == "SW": return s.pw elif pt == "N": return n.mid elif pt == "S": return s.mid elif pt == "E": return e.mid elif pt == "W": return w.mid elif pt == "C": return self.ecx.sect(self.ecy) @property def pne(self): return self.point("NE") @property def pnw(self): return self.point("NW") @property def psw(self): return self.point("SW") @property def pse(self): return self.point("SE") @property def pn(self): return self.point("N") @property def ps(self): return self.point("S") @property def pe(self): return self.point("E") @property def pw(self): return self.point("W") @property def pc(self): return self.point("C") @property def en(self): return self.nsew()[0] @property def es(self): return self.nsew()[1] @property def ee(self): return self.nsew()[2] @property def ew(self): return self.nsew()[3] def trim_start(self): self.pvl() new_start = self._val.value[1][-1][-1] self._val.value[1][0] = "moveTo" self._val.value[1][-1] = [new_start] self._val.value = self._val.value[1:] return self def trim_end(self): self.pvl() end = self._val.value[-1][0] if end in ["closePath", "endPath"]: self._val.value = self._val.value[:-2] if end == "closePath": self.cp() else: self.ep() else: self._val.value = self._val.value[:-1] return self def q2c(self): new_vl = [] for mv, pts in self.v.value: if mv == "qCurveTo": # does not handle all-offcurve+None mode # https://forum.drawbot.com/topic/58/qcurve # https://github.com/fonttools/skia-pathops/issues/45 # https://github.com/fonttools/skia-pathops/issues/71 decomposed = decomposeQuadraticSegment(pts) for dpts in decomposed: qp1, qp2 = [Point(pt) for pt in dpts] try: qp0 = Point(new_vl[-1][-1][-1]) cp1 = qp0 + (qp1 - qp0)*(2.0/3.0) cp2 = qp2 + (qp1 - qp2)*(2.0/3.0) new_vl.append(["curveTo", (cp1, cp2, qp2)]) except Exception as e: print("failed q2c", e) else: new_vl.append([mv, pts]) self.v.value = new_vl return self def flatten(self, length=10, segmentLines=True) -> "P": """ Runs a fontTools `FlattenPen` on this pen """ for el in self._els: el.flatten(length, segmentLines) if self.val_present(): rp = RecordingPen() fp = FlattenPen(rp, approximateSegmentLength=length, segmentLines=segmentLines) self.replay(fp) self._val.value = rp.value return self def smooth(self): for el in self._els: el.smooth() if self.val_present(): rp = RecordingPen() fp = SmoothPointsPen(rp) self.replay(fp) self._val.value = rp.value return self def catmull(self, points, close=False) -> "P": """Run a catmull spline through a series of points""" p0 = points[0] p1, p2, p3 = points[:3] pts = [p0] i = 1 while i < len(points): pts.append([ ((-p0[0] + 6 * p1[0] + p2[0]) / 6), ((-p0[1] + 6 * p1[1] + p2[1]) / 6), ((p1[0] + 6 * p2[0] - p3[0]) / 6), ((p1[1] + 6 * p2[1] - p3[1]) / 6), p2[0], p2[1] ]) p0 = p1 p1 = p2 p2 = p3 try: p3 = points[i + 2] except: p3 = p3 i += 1 self.moveTo(pts[0]) for p in pts[1:]: self.curveTo((p[0], p[1]), (p[2], p[3]), (p[4], p[5])) if close: self.closePath() return self def roughen(self, amplitude=10, threshold=10, ignore_ends=False, seed=None) -> "P": """Randomizes points in skeleton""" if seed is not None: rs = random_series(0, amplitude, seed=seed) else: rs = random_series(0, amplitude, seed=randint(0, 5000)) randomized = [] _x = 0 _y = 0 for idx, (t, pts) in enumerate(self.v.value): if idx == 0 and ignore_ends: randomized.append([t, pts]) continue if idx == len(self.v.value) - 1 and ignore_ends: randomized.append([t, pts]) continue if t == "lineTo" or t == "curveTo": #jx = pnoise1(_x) * amplitude # should actually be 1-d on the tangent (maybe? TODO) #jy = pnoise1(_y) * amplitude jx = rs[idx*2] - amplitude/2 jy = rs[idx*2+1] - amplitude/2 randomized.append([t, [(x+jx, y+jy) for x, y in pts]]) _x += 0.2 _y += 0.3 else: randomized.append([t, pts]) self.v.value = randomized return self def explode(self): """Convert all contours to individual paths""" for el in self._els: el.explode() if self.val_present(): rp = RecordingPen() ep = ExplodingPen(rp) self.replay(ep) for p in ep._pens: el = type(self)() el._val.value = p el._attrs = deepcopy(self._attrs) self.append(el) self._val = RecordingPen() return self def implode(self): # TODO preserve frame from some of this? #self.reset_val() self._val = RecordingPen() for el in self._els: self.record(el._val) self._els = [] return self def map_points(self, fn, filter_fn=None) -> "P": idx = 0 for cidx, c in enumerate(self._val.value): move, pts = c pts = list(pts) for pidx, p in enumerate(pts): x, y = p if filter_fn and not filter_fn(Point(p)): continue result = fn(idx, x, y) if result: pts[pidx] = result idx += 1 self._val.value[cidx] = (move, pts) return self def mod_contour(self, contour_index, mod_fn=None) -> "P": exploded = self.copy().explode() if mod_fn: mod_fn(exploded[contour_index]) self._val.value = exploded.implode()._val.value return self else: return exploded[contour_index] def filterContours(self, filter_fn) -> "P": if self.val_present(): exploded = self.copy().explode() keep = [] for idx, c in enumerate(exploded): if filter_fn(idx, c): keep.append(c) self._val.value = type(self)(keep).implode()._val.value return self def repeat(self, times=1) -> "P": for el in self._els: el.repeat(times) if self.val_present(): copy = self.copy()._val.value _, copy_0_data = copy[0] copy[0] = ("moveTo", copy_0_data) self._val.value = self._val.value[:-1] + copy if times > 1: self.repeat(times-1) return self def outline(self, offset=1, drawInner=True, drawOuter=True, cap="square", miterLimit=None, closeOpenPaths=True ) -> "P": """AKA expandStroke""" for el in self._els: el.outline(offset, drawInner, drawOuter, cap, miterLimit, closeOpenPaths) if self.val_present(): op = OutlinePen(None , offset=offset , optimizeCurve=True , cap=cap , miterLimit=miterLimit , closeOpenPaths=closeOpenPaths) self._val.replay(op) op.drawSettings(drawInner=drawInner , drawOuter=drawOuter) g = op.getGlyph() self._val.value = [] g.draw(self._val) return self ol = outline def project(self, angle, width) -> "P": offset = polarCoord((0, 0), math.radians(angle), width) self.translate(offset[0], offset[1]) return self def castshadow(self, angle=-45, width=100, ro=1, fill=True ) -> "P": for el in self._els: el.castshadow(angle, width, ro, fill) if self.val_present(): out = RecordingPen() tp = TranslationPen(out , frontAngle=angle , frontWidth=width) self._val.replay(tp) if fill: self.copy().project(angle, width)._val.replay(out) #out.record() self._val.value = out.value if ro: self.removeOverlap() return self def understroke(self, s=0, sw=5, outline=False, dofill=0, miterLimit=None ) -> "P": if sw == 0: return self def mod_fn(p): if not outline: return p.fssw(s, s, sw) else: if dofill: pf = p.copy() p.f(s).outline(sw*2, miterLimit=miterLimit) if dofill: p.reverse().record(pf) return p return self.layerv(mod_fn, 1) def runonCast(): def _runonCast(p): return P.FromPens(p) return _runonCast ================================================ FILE: packages/coldtype-core/src/coldtype/runon/runon.py ================================================ import re from textwrap import wrap from typing import Callable, Optional from inspect import signature from random import Random from time import sleep from copy import deepcopy from collections.abc import Iterable from collections import namedtuple from functools import partial from coldtype.fx.chainable import Chainable _ARG_COUNT_CACHE = {} def _arg_count(fn): if fn not in _ARG_COUNT_CACHE: count = len(signature(fn).parameters) _ARG_COUNT_CACHE[fn] = count return _ARG_COUNT_CACHE[fn] RunonEnumerable = namedtuple("RunonEnumerable", ["i", "el", "e", "len", "k", "parent"]) class RunonException(Exception): pass class RunonSearchException(Exception): pass class RunonNoData: pass class Runon: def __init__(self, *val): els = [] to_append = [] if len(val) == 1 and not isinstance(val[0], Runon): if isinstance(val[0], Iterable) and not isinstance(val[0], str): to_append = val[0] value = None else: value = val[0] else: value = None els = [] to_append = val self._val = None self.reset_val() if value is not None: self._val = self.normalize_val(value) self._els = els self._visible = True self._alpha = 1 self._attrs = {} self._data = {} self._parent = None self._tag = None self._tmp_attr_tag = None for el in to_append: self.append(el) def post_init(self): """subclass hook""" pass def yields_wrapped(self): """subclass hook""" return True # Value operations def update(self, val): if callable(val): val = val(self) self._val = val return self @property def v(self): return self._val # Array Operations def _call_idx_fn(self, fn, idx, arg:"Runon"): if not self.yields_wrapped(): try: if arg.val_present(): arg = arg.v except AttributeError: arg = arg ac = _arg_count(fn) if ac == 1: return fn(arg) else: return fn(idx, arg) def _norm_element(self, el): if el is None: return None if callable(el): el = el(self) if not isinstance(el, Runon): el = type(self)(el) return el def append(self, el): el = self._norm_element(el) if el is None: return self if el.data("insert") is not None: self._els.insert(el.data("insert"), el) el.data(insert="delete") else: self._els.append(el) return self å = append def replicate(self, *els): for el in els: self.append(el.copy()) return self def attach(self, parent): parent.append(self) return self def extend(self, els): if callable(els): els = els(self) if isinstance(els, Runon): if len(els) > 0: self.append(els._els) else: self.append(els) else: [self.append(el) for el in els] return self def insert(self, idx, el): el = self._norm_element(el) if el is None: return self parent = self try: p = self for x in idx[:-1]: if len(self) > 0: parent = p p = p[x] p._els.insert(idx[-1], el) return self except TypeError: pass parent._els.insert(idx, el) return self def __iadd__(self, item): """alias to append""" return self.append(item) def __add__(self, item): """yields new Runon with current nested, item after""" return type(self)(self, item) # Generic Interface def __str__(self): return self.__repr__() def printable_val(self): """subclass hook for __repr__""" return self._val def printable_data(self): """subclass hook for __repr__""" return self._data def __repr__(self, **kwargs): v = self.printable_val() t = self._tag d = self.printable_data() l = len(self) if v is None: v = "" else: v = f"({v})" if l == 0: l = "" else: l = "/" + str(l) + "..." if t is None: t = "" else: t = f" {{#{t}}}" if len(d) == 0: d = "" else: if True: ds = [] for k, _v in d.items(): if len(kwargs) == 0 or kwargs.get(k, True): ds.append(f"{k}={_v}") d = " {" + ",".join(ds) + "}" else: d = " {" + ",".join([f"{k}={v}" for k,v in d.items()]) + "}" if self.val_present(): tv = type(self._val).__name__ #if len(tv) > 5: # tv = tv[:5] + "..." else: tv = "" ty = type(self).__name__ if ty == "Runon": ty = "" out = f"<®:{ty}:{tv}{v}{l}{t}{d}>" return out def __bool__(self): return len(self._els) > 0 or self._val is not None def val_present(self): """subclass hook""" return self._val is not None def normalize_val(self, val): """subclass hook""" return val def reset_val(self): self._val = None self._data = {} self._attrs = {} self._tag = None return self def __len__(self): return len(self._els) def __getitem__(self, index): if isinstance(index, int) or isinstance(index, slice): el = self._els[index] else: tag = index el = self.find_(tag) if el and self.data("vend"): return el.copy() return el def get(self, key, default=None): try: return self[key] except RunonSearchException: return default def subset(self, *idxs): """return subset of self wrapped in same type as self (rather than raw list)""" if isinstance(idxs[0], slice): return type(self)(self._els[idxs[0]]) else: out = type(self)() for i in idxs: out.append(self[i%len(self._els)]) return out def __setitem__(self, index, pen): self._els[index] = pen def tree(self, v=True, limit=100, **kwargs): out = [] def walker(el, pos, data): if pos <= 0: if pos == 0 and not v: return dep = data.get("depth", 0) tab = " |"*dep if pos == 0: tab = tab[:-1] + "-" sel = el.__repr__(**kwargs) sel = wrap(sel, limit, initial_indent="", subsequent_indent=" "*(dep+2) + " ") out.append(tab + " " + "\n".join(sel)) self.walk(walker) return "\n" + "\n".join(out) def depth(self): if len(self) > 0: return 1 + max(p.depth() for p in self) else: return 0 # Iteration operations def walk(self, callback:Callable[["Runon", int, dict], None], depth=0, visible_only=False, parent=None, alpha=1, idx=None, _selector=None, ): if visible_only and not self._visible: return if parent: self._parent = parent alpha = self._alpha * alpha if len(self) > 0: utag = "_".join([str(i) for i in idx]) if idx else None if utag is None and parent is None: utag = "ROOT" if _selector is None or _selector == -1: callback(self, -1, dict(depth=depth, alpha=alpha, idx=idx, utag=utag)) for pidx, el in enumerate(self._els): idxs = [*idx] if idx else [] idxs.append(pidx) el.walk(callback, depth=depth+1, visible_only=visible_only, parent=self, alpha=alpha, idx=idxs, _selector=_selector) utag = "_".join([str(i) for i in idx]) if idx else None if utag is None and parent is None: utag = "ROOT" if _selector is None or _selector == +1: callback(self, 1, dict(depth=depth, alpha=alpha, idx=idx, utag=utag)) else: utag = "_".join([str(i) for i in idx]) if idx else None if utag is None and parent is None: utag = "ROOT" res = callback(self, 0, dict( depth=depth, alpha=alpha, idx=idx, utag=utag)) if res is not None: if parent is None: return res else: parent[idx[-1]] = res return self def prewalk(self, callback): return self.walk(callback, _selector=-1) def postwalk(self, callback): return self.walk(callback, _selector=+1) def parent(self, noexist_ok=False): if self._parent: return self._parent else: if not noexist_ok: print("no parent set") return None def match(self, regex, cb=None, partial=False): matches = [] rek = re.compile(regex) def walker(el, _, __): p = el.path(self) if partial: m = rek.match(p) else: m = rek.fullmatch(p) if m: if cb: cb(el) else: matches.append(el) self.prewalk(walker) if not cb: return matches else: return self def map(self, fn, range=None): els = self._els[range] if range is not None else self._els for idx, p in enumerate(els): res = self._call_idx_fn(fn, idx, p) if res: self._els[idx] = res return self def mape(self, fn): total = len(self._els) for idx, p in enumerate(self._els): res = fn(idx/(total-1), p) if res: self._els[idx] = res return self def filter(self, fn): to_delete = [] for idx, p in enumerate(self._els): if isinstance(fn, str): res = p.tag() == fn else: res = self._call_idx_fn(fn, idx, p) if res == False: to_delete.append(idx) to_delete = sorted(to_delete, reverse=True) for idx in to_delete: del self._els[idx] return self def remove(self, fn): to_delete = [] for idx, p in enumerate(self._els): if isinstance(fn, str): res = p.tag() == fn else: res = self._call_idx_fn(fn, idx, p) if res == True: to_delete.append(idx) to_delete = sorted(to_delete, reverse=True) for idx in to_delete: del self._els[idx] return self def mapv(self, fn): if len(self) == 0: if self.val_present(): print("! no els to mapv") return self idx = 0 def walker(el, pos, _): nonlocal idx if pos != 0: return res = self._call_idx_fn(fn, idx, el) idx += 1 return res return self.walk(walker) def mapvrc(self, fn:Callable[[int, int, "Runon"], "Runon"]): """ (map)-(v)alues with (r)ow-and-(c)olumn __only works with spread/stack structure__ """ def walker(p, pos, data:dict): if pos == 0: r, c = data.get("idx", (-1, -1)) return fn(r, c, p) return self.walk(walker) def mapvch(self, fn:Callable[[bool, "Runon"], "Runon"]): """ (map)-(v)alues (ch)eckerboard style """ return self.mapvrc(lambda r, c, p: fn(not((not r%2 and not c%2) or (r%2 and c%2)), p)) def filterv(self, fn): idx = 0 def walker(el, pos, data): nonlocal idx if pos == 0: res = self._call_idx_fn(fn, idx, el) if not res: el.data(_walk_delete=True) idx += 1 return None elif pos == 1: el.filter(lambda p: not p.data("_walk_delete")) return self.walk(walker) def delete(self): self._els = [] self.reset_val() return self def deblank(self): return self.filterv(lambda p: p.val_present()) removeBlanks = deblank def interpose(self, el_or_fn): new_els = [] for idx, el in enumerate(self._els): if idx > 0: if callable(el_or_fn): new_els.append(el_or_fn(idx-1)) else: new_els.append(el_or_fn.copy()) new_els.append(el) self._els = new_els return self def split(self, fn, split=0): out = type(self)() curr = type(self)() for el in self._els: do_split = False if callable(fn): do_split = fn(el) else: if el._val == fn: do_split = True if do_split: if split == -1: curr.append(el) out.append(curr) curr = type(self)() if split == 1: curr.append(el) else: curr.append(el) out.append(curr) self._els = out._els return self def enumerate(self, enumerable, enumerator): es = list(enumerable) length = len(es) if length == 0: return self if isinstance(enumerable, dict): for idx, k in enumerate(enumerable.keys()): item = enumerable[k] if idx == 0 and len(enumerable) == 1: e = 0.5 else: e = idx / (length-1) result = enumerator(RunonEnumerable(idx, item, e, length, k, self)) if result != self: self.append(result) else: for idx, item in enumerate(es): if idx == 0 and length == 1: e = 0.5 else: e = idx / (length-1) result = enumerator(RunonEnumerable(idx, item, e, length, idx, self)) if result != self: self.append(result) return self # Hierarchical Operations def collapse(self, deblank=True): """AKA `flatten` in some programming contexts""" els = [] def walk(el, pos, data): if pos == 0 and (el.val_present() or not deblank): els.append(el) self.walk(walk) self._els = els return self def collapseonce(self, deblank=True): """Same as collapse, except only collapses one-level""" els = [] for el in self._els: els.extend(el._els) self._els = els return self def sum(self): out = [] if self.val_present(): out.append(self._val) r = self.copy().collapse() for el in r._els: out.append(el._val) return out def reverse(self, recursive=False, winding=True): """in-place element reversal""" self._els = list(reversed(self._els)) if recursive: for el in self._els: el.reverse(recursive=True, winding=winding) return self def shuffle(self, seed=0): "in-place shuffle" r = Random() r.seed(seed) r.shuffle(self._els) return self def copy_val(self, val): if hasattr(val, "copy"): return val.copy() else: return val def copy(self, deep=True, with_data=True): """with_data is deprecated""" val_copy = self.copy_val(self._val) _copy = type(self)(val_copy) copied = False if deep: try: _copy._data = deepcopy(self._data) _copy._attrs = deepcopy(self._attrs) copied = True except TypeError: pass if not copied: _copy._data = self._data.copy() _copy._attrs = self._attrs.copy() _copy._tag = self._tag # kind of a hack but necessary if hasattr(self, "_stst"): _copy._stst = self._stst for el in self._els: _copy.append(el.copy()) return _copy def index(self, idx, fn=None): parent = self lidx = idx try: p = self for x in idx: if len(p) > 0: parent = p lidx = x p = p[x] else: res = p.index(x, fn) if not fn: return res else: return self except TypeError as e: try: p = self[idx] except IndexError: return self if fn: res = self._call_idx_fn(fn, lidx, p) if res is not None: parent[lidx] = res else: return parent[lidx] return self def indices(self, idxs, fn=None): out = [] for idx in idxs: out.append(self.index(idx, fn)) if fn is None: return out return self def î(self, idx, fn=None): return self.index(idx, fn) def ï(self, idxs, fn=None): return self.indices(idxs, fn) def find(self, finder_fn, fn=None, index=None, find_one=False, ): matches = [] found_one = [] def finder(p, pos, data): if len(found_one) > 0: return #if limit and len(matches) > limit: # return found = False if pos <= 0 and data["depth"] > 0: if isinstance(finder_fn, str): found = p.tag() == finder_fn elif callable(finder_fn): found = finder_fn(p) else: found = all(p.data(k) == v for k, v in finder_fn.items()) if found: matches.append([p, data["depth"]]) if find_one: found_one.append(True) self.walk(finder) #matches = list(reversed(sorted(matches, key=lambda m: m.depth()))) matches = list([m[0] for m in sorted(matches, key=lambda m: m[1])]) narrowed = [] if index is not None: for idx, match in enumerate(matches): if isinstance(index, int): if idx == index: narrowed.append([idx, match]) else: if idx in index: narrowed.append([idx, match]) else: for idx, match in enumerate(matches): narrowed.append([idx, match]) if fn: for idx, match in narrowed: self._call_idx_fn(fn, idx, match) if fn: return self else: return [m for (_, m) in narrowed] def find_(self, finder_fn=None, fn=None, index=0, none_ok=0, find_one=False, **kwargs): if len(kwargs) > 0 and finder_fn is None: finder_fn = kwargs if isinstance(finder_fn, str): if "+" in finder_fn: finders = finder_fn.split("+") parts = [] for finder in finders: parts.append(self.find_(finder, none_ok=1)) return type(self)([p for p in parts if p]) elif "/" in finder_fn: o = self for k in finder_fn.split("/"): if k == "<": o = self.parent() continue o = o.find_(k) return o res = self.find(finder_fn, fn, index=index, find_one=find_one) if not fn: try: return res[0] except IndexError: if none_ok: return None raise RunonSearchException(f"Could not find `{finder_fn}`") else: return self def replace(self, tag, replacement, limit=None): if isinstance(tag, str): def walker(p, pos, data): if pos in [0, 1]: if p.tag() == tag: self.index(data["idx"], replacement) return self.walk(walker) elif callable(tag): def walker(p, pos, data): if pos in [0, 1]: if tag(p): self.index(data["idx"], replacement) return self.walk(walker) else: raise Exception("not yet supported") def swap(self, indices, replace_fn): def _replace_fn(p): pc = p.copy() p.delete() p.up() p.append(replace_fn(pc)) return self.index(indices, _replace_fn) def partition(self, fn): last = None group = type(self)() out = type(self)() for p in self: v = fn(p) if last == v: group.append(p) else: if len(group) > 0: out.append(group) group = type(self)() group.append(p) last = v out.append(group) return out # Data-access methods def data(self, key=None, default=None, function_literals=False, **kwargs): """Set with kwargs, read with key= & default=""" if key is None and len(kwargs) > 0: for k, v in kwargs.items(): if v == "delete": del self._data[k] else: if not function_literals and callable(v): v = self._call_idx_fn(v, k, self) self._data[k] = v return self elif key is None and kwargs is not None: return self elif key is not None: return self._data.get(key, default) else: return self._data def datafn(self, **kwargs): """Set function literals with kwargs""" return self.data(function_literals=True, **kwargs) def tag(self, value=RunonNoData()): if isinstance(value, RunonNoData): return self._tag else: self._tag = value return self def path(self, root=None): tags = [self.tag()] parent = self.parent(noexist_ok=True) while parent and parent != root: tags.insert(0, parent.tag()) parent = parent.parent(noexist_ok=True) return "/".join([t for t in tags if t]) def style(self, style="_default"): if style and style in self._attrs: return self._attrs[style] else: return self._attrs.get("_default", {}) def styles(self): all = {} for k, _ in self._attrs.items(): style = self.style(k) if k == "_default": all["default"] = style else: all[k] = style return all @property # deprecated / backwards-compatibility def attrs(self): return self.styles() def normalize_attr_value(self, k, v): """subclass hook""" return v def attr(self, tag=None, field=None, recursive=True, **kwargs ): if field is None and len(kwargs) == 0: field = tag tag = None if tag is None: if self._tmp_attr_tag is not None: tag = self._tmp_attr_tag else: tag = "_default" if tag == "default": tag = "_default" if field: # getting, not setting return self._attrs.get(tag, {}).get(field) attrs = self._attrs.get(tag, {}) for k, v in kwargs.items(): attrs[k] = self.normalize_attr_value(k, v) self._attrs[tag] = attrs if recursive: for el in self._els: el.attr(tag=tag, field=None, recursive=True, **kwargs) return self def cast(self, _class, *args): """Quickly cast to a (different) subclass.""" res = _class(self, *args) res._attrs = deepcopy(self._attrs) return res def lattr(self, tag, fn): """temporarily change default tag to something other than 'default'""" self._tmp_attr_tag = tag fn(self) self._tmp_attr_tag = None return self def _get_set_prop(self, prop, v, castfn=None): if v is None: return getattr(self, prop) _v = v if callable(v): _v = v(self) if castfn is not None: _v = castfn(_v) setattr(self, prop, _v) return self def visible(self, v=None): return self._get_set_prop("_visible", v, bool) def alpha(self, v=None): return self._get_set_prop("_alpha", v, float) def hide(self, *indices): if len(indices) > 0: for idx in indices: self.index(idx, lambda p: p.visible(False)) else: self.visible(False) if self.val_present(): self.visible(False) return self def __neg__(self): return self.hide() # Logic Operations def cond(self, condition, if_true:Callable[["Runon"], None], if_false:Callable[["Runon"], None]=None ): if callable(condition): condition = condition(self) if condition: if callable(if_true): if_true(self) else: #self.gs(if_true) pass # TODO? else: if if_false is not None: if callable(if_false): if_false(self) else: #self.gs(if_false) pass # TODO? return self def attempt(self, exception_class, try_fn, except_fn): try: try_fn(self) except exception_class: except_fn(self) # Chaining def chain(self, fn:Callable[["Runon"], None], *args ): """ For simple take-one callback functions in a chain """ if fn: if isinstance(fn, Chainable): res = fn.func(self, *args) if res: return res return self try: if isinstance(fn[0], Chainable): r = self for f in fn: r = r.chain(f, *args) return r except TypeError: pass try: fn, metadata = fn except TypeError: metadata = {} # So you can pass in a function # without calling it (if it has no args) # TODO what happens w/ no args but kwargs? ac = _arg_count(fn) if ac == 0: fn = fn() res = fn(self, *args) if "returns" in metadata: return res elif isinstance(res, Runon): return res elif res: return res return self ch = chain def __or__(self, other): return self.chain(other) def __ror__(self, other): return self.chain(other) def __truediv__(self, other): return self.mapv(other) def __sub__(self, other): """noop""" return self def up(self): """up one level of hierarchy""" copy = self.copy() self.reset_val() self._els = [copy] return self ups = up def down(self): """down one level of hierarchy — unimplemented by Runon""" return self def replicate(self, cells, mod=None): return (type(self)().enumerate(cells, lambda x: self.copy().align(x.el).ch(partial(mod, x.el) if mod else None))) def layer(self, *layers): if len(layers) == 1 and isinstance(layers[0], int): layers = [1]*layers[0] els = [] for layer in layers: try: c = layer[0] for x in range(0, c): els.append(layer[1](x, self.copy())) except TypeError: if callable(layer): els.append(layer(self.copy())) elif isinstance(layer, Chainable): els.append(layer.func(self.copy())) else: els.append(self.copy()) self.reset_val() self._els = els return self def layerv(self, *layers): if self.val_present(): if len(layers) == 1 and isinstance(layers[0], int): layers = [1]*layers[0] els = [] for layer in layers: if callable(layer): els.append(layer(self.copy())) elif isinstance(layer, Chainable): els.append(layer.func(self.copy())) else: els.append(self.copy()) self.reset_val() self.extend(els) else: for el in self._els: el.layerv(*layers) return self def overwrite(self, fn): self.layer(fn) return self[0] # Utils def declare(self, *whatever): # TODO do something with what's declared somehow? return self def print(self, *args, **kwargs): if len(args) == 0: print(self.tree(**kwargs)) return self out = [] for a in args: if callable(a): out.append(str(a(self))) else: out.append(str(a)) print(" ".join(out)) return self def pprint(self, *args): if len(args) == 0: print(self.tree()) return self from pprint import pprint for a in args: if callable(a): pprint(a(self)) else: pprint(a) return self def printh(self): """print hierarchy, no values""" print(self.tree(v=False)) return self def printdata(self, field): print(self.data(field)) return self def noop(self, *args, **kwargs): """Does nothing""" return self def null(self): """For chaining; return an empty instead of this pen""" self.reset_val() self._els = [] return self def sleep(self, time): """Sleep call within the chain (if you want to measure something)""" sleep(time) return self # Aliases pmap = mapv ================================================ FILE: packages/coldtype-core/src/coldtype/runon/scaffold.py ================================================ from coldtype.runon import Runon from coldtype.geometry import Rect from coldtype.grid import Grid from coldtype.color import hsl, bw from coldtype.random import random_series from collections import defaultdict import re _view_rs1 = None class Scaffold(Runon): @staticmethod def AspectGrid(r:Rect, x:int, y:int, align:str="C"): s = Scaffold(r.fit_aspect(x, y, align)) return s.numeric_grid(x, y) def __init__(self, *val, warn_float=True): self.warn_float = warn_float super().__init__(*val) def find_(self, finder_fn=None, fn=None, index=0, none_ok=0, find_one=False, **kwargs): if isinstance(finder_fn, str): def to_xy(xy): return [int(n) for n in xy.split("|")] def wrap(xy): x, y = to_xy(xy) w = len(self.cols()) h = len(self.rows()) if x < 0: x = w + x if y < 0: y = h + y return f"{x}|{y}" if "*" in finder_fn: start, extend = finder_fn.split("*") start = wrap(start) start = to_xy(start) extend = to_xy(extend) x, y = start ex, ey = extend end = [x+(ex-1), y+(ey-1)] if ex == 0: end[0] = start[0] elif ex < 0: end[0] = x+(ex+1) if ey == 0: end[1] = start[1] elif ey < 0: end[1] = y+(ey+1) finder_fn = f"{start[0]}|{start[1]}+{end[0]}|{end[1]}" elif "-" in finder_fn: finder_fn = wrap(finder_fn) return super().find_(finder_fn=finder_fn, fn=fn, index=index, none_ok=none_ok, find_one=find_one, **kwargs) def sum(self): if self.val_present(): return self._val r = None for el in self._els: if r: rr = el.rect if rr.nonzero(): r = r.union(rr) else: rr = el.rect if rr.nonzero(): r = rr if r is not None: return r else: return Rect(0,0,0,0) @property def rect(self) -> Rect: return self.sum() r = rect def _extend_with_tags(self, rects, tags): for idx, r in enumerate(rects): el = Scaffold(r, warn_float=self.warn_float) try: el.tag(tags[idx]) except IndexError: pass self.append(el) def divide(self, amt, edge, tags=[], forcePixel=False): if self.val_present(): self._extend_with_tags( self.r.divide(amt, edge, forcePixel), tags) self._val = None else: for el in self._els: el.divide(amt, edge, tags=tags, forcePixel=forcePixel) return self def subdivide(self, amt, edge, tags=[]): if self.val_present(): self._extend_with_tags( self.r.subdivide(amt, edge), tags) self._val = None else: for el in self._els: el.subdivide(amt, edge, tags) return self def annotate_rings(self): grid_data = self.data("grid") columns = grid_data.get("columns") rows = grid_data.get("rows") def rectangular_rings(xs, ys): center_x, center_y = xs // 2, ys // 2 max_radius = max(center_x, center_y) rings = [] for r in range(max_radius + 1): ring = [ self[f"{x}|{y}"].data(ring=r, ring_e=r/max_radius) for x in range(center_x - r, center_x + r + 1) for y in range(center_y - r, center_y + r + 1) if ( (x == center_x - r or x == center_x + r or y == center_y - r or y == center_y + r) and 0 <= x < xs and 0 <= y < ys)] if ring: rings.append(ring) return rings return rectangular_rings(columns, rows) def numeric_grid(self, columns=2, rows=None, gap=None, column_gap=0, row_gap=0, start_1=False, start_bottom=True, annotate_rings=False ): if rows is None: rows = columns if gap is not None: column_gap = gap row_gap = gap self.data(grid=dict(columns=columns, rows=rows)) names = [] if column_gap == 0 and row_gap == 0: for ridx, row in enumerate(range(0, rows)): row_names = [] for cidx, col in enumerate(range(0, columns)): row_names.append(f"{cidx}|{ridx}") names.append(" ".join(row_names)) if start_bottom: names = list(reversed(names)) self.cssgrid(("a " * columns).strip(), ("a " * rows).strip(), " / ".join(names)) else: _cols = [] _rows = [] for ridx, row in enumerate(range(0, rows)): _rows.append("a") _cols = [] row_names = [] for cidx, col in enumerate(range(0, columns)): _cols.append("a") row_names.append(f"{cidx}|{ridx}") if cidx != columns-1: _cols.append(str(column_gap)) row_names.append(f"cg.{cidx}|{ridx}") names.append(" ".join(row_names)) if ridx != rows-1: _rows.append(str(row_gap)) row_names = [] for cidx, col in enumerate(range(0, columns)): row_names.append(f"rg.{cidx}|{ridx}") if cidx != columns-1: row_names.append(f"rcg.{cidx}|{ridx}") names.append(" ".join(row_names)) if start_bottom: names = list(reversed(names)) self.cssgrid(" ".join(_cols).strip(), " ".join(_rows), " / ".join(names), forcePixel=True) if not hasattr(self, "_borders"): self._borders = [] first_row = names[0].split(" ") last_row = names[-1].split(" ") for cidx, _ in enumerate(first_row[:-1]): self._borders.append([self[first_row[cidx]].pne, self[last_row[cidx]].pse]) for ridx, row in enumerate(names[:-1]): rs = row.split(" ") self._borders.append([self[rs[0]].psw, self[rs[-1]].pse]) for cell in self: tag = cell.tag() if "." not in tag: c, r = [int(x) for x in cell.tag().split("|")] ch = not((not r%2 and not c%2) or (r%2 and c%2)) if start_1: r += 1 c += 1 cell.data(row=r, col=c, checker=ch) if annotate_rings: self.annotate_rings() return self def labeled_grid(self, columns=2, rows=2, column_gap=0, row_gap=0, start_1=False, start_bottom=False): from string import ascii_lowercase possible_names = [a for a in ascii_lowercase] possible_names.extend([f"{a}{a}" for a in ascii_lowercase]) possible_names.extend([f"{a}{a}{a}" for a in ascii_lowercase]) if start_1: inc = 1 else: inc = 0 names = [] if column_gap == 0 and row_gap == 0: for ridx, row in enumerate(range(0, rows)): row_names = [] for cidx, col in enumerate(range(0, columns)): row_names.append(f"{possible_names[ridx]}{cidx+inc}") names.append(" ".join(row_names)) if start_bottom: names = list(reversed(names)) self.cssgrid(("a " * columns).strip(), ("a " * rows).strip(), " / ".join(names)) else: _cols = [] _rows = [] for ridx, row in enumerate(range(0, rows)): _rows.append("a") _cols = [] row_names = [] for cidx, col in enumerate(range(0, columns)): _cols.append("a") row_names.append(f"{ascii_lowercase[ridx]}{cidx+inc}") if cidx != columns-1: _cols.append(str(column_gap)) row_names.append(f"cg.{ascii_lowercase[ridx]}{cidx+inc}") names.append(" ".join(row_names)) if ridx != rows-1: _rows.append(str(row_gap)) row_names = [] for cidx, col in enumerate(range(0, columns)): row_names.append(f"rg.{ascii_lowercase[ridx]}{cidx+inc}") if cidx != columns-1: row_names.append(f"rcg.{ascii_lowercase[ridx]}{cidx+inc}") names.append(" ".join(row_names)) if start_bottom: names = list(reversed(names)) self.cssgrid(" ".join(_cols).strip(), " ".join(_rows), " / ".join(names)) if not hasattr(self, "_borders"): self._borders = [] first_row = names[0].split(" ") last_row = names[-1].split(" ") for cidx, _ in enumerate(first_row[:-1]): self._borders.append([self[first_row[cidx]].pne, self[last_row[cidx]].pse]) for ridx, row in enumerate(names[:-1]): rs = row.split(" ") self._borders.append([self[rs[0]].psw, self[rs[-1]].pse]) for cell in self: tag = cell.tag() if "." not in tag: _r = re.search(r"[a-z]{1,3}", cell.tag())[0] _c = re.search(r"[0-9]{1,3}", cell.tag())[0] #_r, _c = list(cell.tag()) r = possible_names.index(_r) c = int(_c) ch = not((not r%2 and not c%2) or (r%2 and c%2)) if start_1: r += 1 c += 1 cell.data(row=r, col=c, checker=ch) return self def cells(self): return self.copy().filter(lambda x: "." not in x.tag()) def gaps(self): return self.copy().filter(lambda x: "." in x.tag()) def rows(self): rows = defaultdict(list) for cell in self: row = cell.data("row") if row is not None: rows[row].append(cell.copy()) return list(rows.values()) def cols(self): cols = defaultdict(list) for cell in self: col = cell.data("col") if col is not None: cols[col].append(cell.copy()) return list(cols.values()) def grid(self, columns=2, rows=None, tags=[]): if rows is None: rows = columns if self.val_present(): self._extend_with_tags( self.r.grid(columns, rows), tags) if not hasattr(self, "_borders"): self._borders = [] for _x in range(0, columns-1): self._borders.append([ self[_x].ee.pn, self[_x].ee.intersection(self.r.es)]) for _y in range(0, rows-1): self._borders.append([ self[_y*columns].psw, self[_y*columns].es.intersection(self.r.ee)]) self._val = None else: for el in self._els: el.grid(columns, rows, tags) return self def cssgridmod(self, mods, **kwargs): for k, v in mods.items(): for m in self.match(k): m.cssgrid(*v) for k, v in kwargs.items(): self[k].cssgrid(*v) return self def cssgrid(self, cols, rows, ascii, mods={}, forcePixel=False, **kwargs): if self.val_present(): if not hasattr(self, "_borders"): self._borders = [] r = self.r if callable(cols): cols = cols(self) if callable(rows): rows = rows(self) g = Grid(self.r, cols, rows, ascii, warn_float=self.warn_float, forcePixel=forcePixel) for k, v in g.keyed.items(): if v.w > r.w or v.h > r.h or v.x < 0 or v.y < 0 or v.w < 0 or v.h < 0: v = Rect(0, 0, 0, 0) self.append(Scaffold(v, warn_float=self.warn_float).tag(k)) self._val = None self._borders.extend(g.borders) self.cssgridmod(mods, **kwargs) return self def borders(self): if hasattr(self, "_borders"): from coldtype.runon.path import P return (P().enumerate(self._borders, lambda x: P().line(x.el).fssw(-1, 0, 1))) def cssborders(self, regular=None, bold=None): if hasattr(self, "_borders"): from coldtype.runon.path import P out = P() for b in sorted(self._borders, key=lambda b: b[2]): if regular == -1 and not b[2]: continue elif bold == -1 and b[2]: continue out.append(P() .line(b[0].edge(b[1])) .fssw(-1, 0, 4 if b[2] else 2) .ch(bold if b[2] else regular)) return out def sort(self, attr="x", reverse=False): if self.depth() == 1: self._els = sorted(self._els, key=lambda el: getattr(el.r, attr), reverse=reverse) return self def view(self, fontSize=22, fill=False, stroke=True, vectors=False): global _view_rs1 if _view_rs1 is None: _view_rs1 = random_series() from coldtype.runon.path import P from coldtype.text import Style, StSt, Font out = P() ridx = 0 def walker(el, pos, data): nonlocal ridx if pos == 0: out.append(P( P(el.r).f(hsl(_view_rs1[ridx], 1)).alpha(0.1) if fill else None, P(el.r.inset(0)).fssw(-1, bw(0, 0.2), 1) if stroke else None, P().text(el.tag() or ("u:" + data["utag"]) , Style("Monaco", fontSize, load_font=0, fill=bw(0, 0.5)) , el.r.inset(7, 10)) if not vectors else StSt(el.tag(), Font.JBMono(), fontSize) .scaleToRect(el.r, shrink_only=True) .align(el.r.inset(2), "SW"))) ridx += 1 self.postwalk(walker) return out # @property # def ne(self): return self.r.pne # @property # def nw(self): return self.r.pnw # @property # def sw(self): return self.r.psw # @property # def se(self): return self.r.pse # @property # def n(self): return self.r.pn # @property # def s(self): return self.r.ps # @property # def e(self): return self.r.pe # @property # def w(self): return self.r.pw @property def pne(self): return self.r.pne @property def pnw(self): return self.r.pnw @property def psw(self): return self.r.psw @property def pse(self): return self.r.pse @property def pn(self): return self.r.pn @property def ps(self): return self.r.ps @property def pe(self): return self.r.pe @property def pw(self): return self.r.pw @property def pc(self): return self.r.pc @property def en(self): return self.r.en @property def es(self): return self.r.es @property def ee(self): return self.r.ee @property def ew(self): return self.r.ew def joinp(self, regex): from coldtype.runon.path import P return P().enumerate(self.match(regex), lambda x: P(x.el.r)) def scale(self, scale): def walker(el, pos, _): if pos == 0: el._val = el._val.scale(scale, scale) self.walk(walker) return self ================================================ FILE: packages/coldtype-core/src/coldtype/skiashim.py ================================================ try: import skia SKIA_87 = int(skia.__version__.split(".")[0]) <= 87 except ImportError: skia = None SKIA_87 = False def image_makeShader(image, matrix): if SKIA_87: return image.makeShader(skia.TileMode.kRepeat, skia.TileMode.kRepeat, matrix) else: return image.makeShader(skia.TileMode.kRepeat, skia.TileMode.kRepeat, skia.SamplingOptions(), matrix) def canvas_drawImage(canvas, image, x, y, paint=None): if SKIA_87: canvas.drawImage(image, x, y, paint) else: so = skia.SamplingOptions() #so = skia.SamplingOptions(skia.FilterMode.kLinear, skia.MipmapMode.kLinear) so = skia.SamplingOptions(skia.CubicResampler.CatmullRom()) #so = skia.SamplingOptions(skia.CubicResampler.Mitchell()) canvas.drawImage(image, x, y, so, paint) def imageFilters_Blur(xblur, yblur): if SKIA_87: return skia.BlurImageFilter.Make(xblur, yblur) else: return skia.ImageFilters.Blur(xblur, yblur) def paint_withFilterQualityHigh(): if SKIA_87: return skia.Paint(AntiAlias=True, FilterQuality=skia.FilterQuality.kHigh_FilterQuality) else: #SamplingOptions=skia.SamplingOptions(skia.CubicResampler.Mitchell()) return skia.Paint(AntiAlias=True) def image_resize(img, width, height): if SKIA_87: return img.resize(width, height) else: so = skia.SamplingOptions(skia.CubicResampler.CatmullRom()) #so = skia.SamplingOptions() return img.resize(width, height, so) def make_improved_noise(e, xo, yo, xs, ys, x, y, scale, base): if SKIA_87: noise = skia.PerlinNoiseShader.MakeImprovedNoise(x, y, scale, base) else: noise = skia.PerlinNoiseShader.MakeTurbulence(x, y, scale, base) matrix = skia.Matrix() matrix.setTranslate(e*xo, e*yo) #matrix.setRotate(45, 0, 0) matrix.setScaleX(xs) matrix.setScaleY(ys) return noise.makeWithLocalMatrix(matrix) ================================================ FILE: packages/coldtype-core/src/coldtype/test.py ================================================ import pytest from coldtype import * from functools import partial from random import Random from pprint import pprint co = Font.Cacheable("assets/ColdtypeObviously-VF.ttf") mutator = Font.Cacheable("assets/MutatorSans.ttf") recmono = Font.Cacheable("assets/RecMono-CasualItalic.ttf") Style.RegisterShorthandPrefix("≈", "~/Type/fonts/fonts") def add_grid(render, result): return P([ P(result), P().gridlines(render.rect).s(0, 0.1).sw(1).f(None) ]) def show_error(r, txt): return StyledString(txt.upper(), Style(mutator, 100)).pen().align(r) class test(renderable): def __init__(self, rect=(800, 200), bg=hsl(0.17, 0.8, 0.7, 0.2), post_preview=add_grid, **kwargs): if isinstance(rect, int): rect = (800, rect) super().__init__(rect=rect, bg=bg, post_preview=post_preview, **kwargs) ================================================ FILE: packages/coldtype-core/src/coldtype/text/__init__.py ================================================ from coldtype.geometry.rect import Rect from coldtype.text.reader import Style, StyledString, SegmentedString, normalize_font_path, Font, FontNotFoundException from coldtype.text.composer import Slug, Lockup, Graf, GrafStyle, T2L, Composer, StSt, Glyphwise, Glyphwise2, GlyphwiseGlyph #from coldtype.text.richtext import RichText ================================================ FILE: packages/coldtype-core/src/coldtype/text/colr/brsurface.py ================================================ from contextlib import contextmanager from fontTools.misc.arrayTools import calcBounds from fontTools.misc.transform import Identity from fontTools.pens.recordingPen import RecordingPen from fontTools.pens.transformPen import TransformPen from blackrenderer.backends.base import Canvas, Surface class BRPathCollectorRecordingPen(RecordingPen): def annotate(self, method, data): self.method = method self.data = data def __repr__(self): return f"PathCollectorRecordingPen({self.method}{list(self.data.keys())})" class BRPathCollectorCanvas(Canvas): def __init__(self): self.init() def init(self): self.paths = [] self.currentTransform = Identity def _addPath(self, path, method, data): if self.currentTransform != Identity: path = transformPath(path, self.currentTransform) path.annotate(method, data) self.paths.append(path) def newPath(self): return BRPathCollectorRecordingPen() @contextmanager def savedState(self): savedTransform = self.currentTransform yield self.currentTransform = savedTransform @contextmanager def compositeMode(self, compositeMode): yield def transform(self, transform): self.currentTransform = self.currentTransform.transform(transform) def clipPath(self, path): self._addPath(path, "clipPath", dict()) def drawPathSolid(self, path, color): self._addPath(path, "drawPathSolid", dict(color=color)) def drawPathLinearGradient( self, path, colorLine, pt1, pt2, extendMode, gradientTransform ): self._addPath(path, "drawPathLinearGradient", dict( colorLine=colorLine, pt1=pt1, pt2=pt2, extendMode=extendMode, gradientTransform=gradientTransform, )) def drawPathRadialGradient( self, path, colorLine, startCenter, startRadius, endCenter, endRadius, extendMode, gradientTransform, ): self._addPath(path, "drawPathRadialGradient", dict( colorLine=colorLine, startCenter=startCenter, startRadius=startRadius, endCenter=endCenter, endRadius=endRadius, extendMode=extendMode, gradientTransform=gradientTransform, )) def drawPathSweepGradient( self, path, colorLine, center, startAngle, endAngle, extendMode, gradientTransform, ): self._addPath(path, "drawPathSweepGradient", dict( colorLine=colorLine, center=center, startAngle=startAngle, endAngle=endAngle, extendMode=extendMode, gradientTransform=gradientTransform, )) class BRPathCollectorSurface(Surface): fileExtension = None def __init__(self): self.paths = None pass @contextmanager def canvas(self, boundingBox): canvas = BRPathCollectorCanvas() yield canvas self.paths = canvas.paths def saveImage(self, path): raise Exception("PathCollectorSurface cannot be saved") def transformPath(path, transform): transformedPath = RecordingPen() tpen = TransformPen(transformedPath, transform) path.replay(tpen) # to keep the path ref the same path.value = transformedPath.value return path ================================================ FILE: packages/coldtype-core/src/coldtype/text/colr/skia.py ================================================ import skia from blackrenderer.backends.skia import _unpackColorLine, _extendModeMap class SkiaShaders(): @staticmethod def drawPathLinearGradient(colorLine, pt1, pt2, extendMode, gradientTransform) -> skia.GradientShader: matrix = skia.Matrix() matrix.setAffine(gradientTransform) colors, stops = _unpackColorLine(colorLine) return skia.GradientShader.MakeLinear( points=[pt1, pt2], colors=colors, positions=stops, mode=_extendModeMap[extendMode], localMatrix=matrix, ) @staticmethod def drawPathSweepGradient( colorLine, center, startAngle, endAngle, extendMode, gradientTransform, ): # The following is needed to please the Skia shader, but it's a bit fuzzy # to me how this affects the spec. Translated from: # https://source.chromium.org/chromium/chromium/src/+/master:third_party/skia/src/ports/SkFontHost_FreeType_common.cpp;l=673-686 startAngle %= 360 endAngle %= 360 if startAngle >= endAngle: endAngle += 360 matrix = skia.Matrix() matrix.setAffine(gradientTransform) colors, stops = _unpackColorLine(colorLine) return skia.GradientShader.MakeSweep( cx=center[0], cy=center[1], colors=colors, positions=stops, mode=_extendModeMap[extendMode], startAngle=startAngle, endAngle=endAngle, localMatrix=matrix, ) @staticmethod def drawPathRadialGradient( colorLine, startCenter, startRadius, endCenter, endRadius, extendMode, gradientTransform, ): matrix = skia.Matrix() matrix.setAffine(gradientTransform) colors, stops = _unpackColorLine(colorLine) return skia.GradientShader.MakeTwoPointConical( start=startCenter, startRadius=startRadius, end=endCenter, endRadius=endRadius, colors=colors, positions=stops, mode=_extendModeMap[extendMode], localMatrix=matrix, ) ================================================ FILE: packages/coldtype-core/src/coldtype/text/composer.py ================================================ from coldtype.runon.path import P from coldtype.geometry import Rect, Point from coldtype.text.shaper import segment from coldtype.text.reader import Style, StyledString, FittableMixin, Font, SegmentedString from collections import namedtuple from typing import Callable, Any class GrafStyle(): def __init__(self, leading=10, x="centerx", xp=0, width=0, **kwargs): self.leading = kwargs.get("l", leading) self.x = x self.xp = xp self.width = width class Graf(): def __init__(self, lines, container, style=None, no_frames=False, **kwargs): if isinstance(container, Rect): self.container = P().rect(container) else: self.container = container if style and isinstance(style, GrafStyle): self.style = style elif style and isinstance(style, int): self.style = GrafStyle(leading=style) else: self.style = GrafStyle(**kwargs) self.no_frames = no_frames self.lines = lines def lineRects(self): # which came first, the height or the width??? rects = [] leadings = [] box = self.container.ambit() leftover = box for l in self.lines: box, leftover = leftover.divide(l.height(), "maxy", forcePixel=True) if self.style.leading < 0: # need to add pixels back to leftover leftover.h += abs(self.style.leading) else: leading, leftover = leftover.divide(self.style.leading, "maxy", forcePixel=True) leadings.append(leading) rects.append(box) return rects def width(self): if self.style.width > 0: return self.style.width else: return max([l.width() for l in self.lines]) def fit(self, width=None): rects = self.lineRects() for idx, l in enumerate(self.lines): if width: fw = width else: fw = rects[idx].w - self.style.xp l.fit(fw) return self def pens(self): rects = self.lineRects() pens = P() for idx, l in enumerate(self.lines): r = rects[idx] dps = l.pens().translate(r.x, r.y) # r.x if not self.no_frames: dps.data(frame=Rect(r.x, r.y, r.w, r.h)) pens.append(dps) return pens class Lockup(FittableMixin): def __init__(self, slugs, preserveLetters=True, nestSlugs=True): self.slugs = slugs self.preserveLetters = preserveLetters self.nestSlugs = nestSlugs def __repr__(self): return f"" def width(self): return sum([s.width() for s in self.slugs]) def height(self): return max([s.height() for s in self.slugs]) def textContent(self): return "/".join([s.textContent() for s in self.slugs]) def shrink(self): adjusted = False for s in self.slugs: adjusted = s.shrink() or adjusted return adjusted def pens(self): pens = [] x_off = 0 for s in self.slugs: try: x_off += s.margin[0] except: pass if self.preserveLetters: dps = s.pens() dps.translate(x_off, 0) if self.nestSlugs: pens.append(dps) else: pens.extend(dps._els) else: dps = s.pen() dps.translate(x_off, 0) pens.append(dps) x_off += dps.ambit(tx=0).w try: x_off += s.margin[1] x_off += s.strings[-1].tracking except: pass return P(pens) def pen(self): return self.pens().pen() def TextToLines(text, primary, fallback=None): lines = [] for line in text.split("\n"): lines.append(Lockup([Slug(line, primary, fallback)])) return lines def SlugsToLines(slugs): return [Lockup([slug]) for slug in slugs] def T2L(text, primary, fallback=None): return Lockup.TextToLines(text, primary, fallback) class Slug(SegmentedString): def __init__(self, text, primary, fallback=None, print_segments=False): self.text = text self.primary = primary self.fallback = fallback self.strings = [] self.print_segments = print_segments self.tag() def tag(self): if self.fallback: segments = segment(self.text, "LATIN", print_characters=self.print_segments) if self.print_segments: print(">>>", segments) self.strings = [StyledString(s[1], self.fallback if "LATIN" in s[0] else self.primary) for s in segments] else: self.strings = [StyledString(self.text, self.primary)] def pen(self): return self.pens().pen() def LineSlugs(text, primary, fallback=None): lines = [] for line in text.split("\n"): lines.append(Slug(line, primary, fallback)) return lines class Composer(): """ For multiline text lockups """ def __init__(self, rect:Rect, text:str, style:Style, leading=10, fit=None): lockups = Slug.LineSlugs(text, style) self.rect = rect self.graf = Graf(lockups, self.rect, leading=leading) if fit is not None: self.graf.fit(fit) def pens(self): """ Structured representation of the multi-line text In the result, each line will be a `P`, then within those lines, each glyph/ligature for that line will be an individual `P` """ return self.graf.pens() def pen(self): """ Entire multiline text as a single vector """ return self.graf.pens().pen() def StSt(text, font, font_size=24, rect=Rect(1080, 1080), strip=False, multiline=False, lead=True, #xa="mdx", **kwargs) -> P: """Set a line of text with a single Style object, passed either with it’s constituent parts (i.e. kwargs) or as an actual `Style` object. #### Examples: ```python @renderable() def stst_1(r): # here the styling arguments are "flat" return (StSt("COLDTYPE", Font.ColdObvi(), 100, wdth=0) .align(r)) ``` ```python @renderable() def stst_2(r): # here the styling arguments are encapsulated in the Style object return (StSt("COLDTYPE", Style(Font.ColdObvi(), 100, wdth=1)) .align(r)) ``` """ if not isinstance(text, str): text = "\n".join(text) else: if strip: text = text.strip() styles = [] if isinstance(font, Style): style = font elif isinstance(font, dict): style = Style(**{**font, **kwargs}) elif isinstance(font, list) and isinstance(font[0], Style): style = font[0] styles = font else: style = Style(font, font_size, **kwargs) fit = kwargs.get("fit", None) leading = kwargs.get("leading", 10) if "\n" in text: lines = P() for idx, l in enumerate(text.split("\n")): if styles: _style = styles[idx] else: _style = style lines.append(StSt(l, _style, rect=rect, strip=strip, **{**kwargs, **dict(multline=False)})) if not lead: return lines else: return lines.stack(leading) else: if style.fallback: lockup = Slug(text, style, style.fallback) if fit: lockup.fit(fit) lockup = lockup.pens(flat=False) else: lockup = StyledString(text, style) if fit: lockup.fit(fit) lockup = lockup.pens() if multiline: return P([lockup]) #lockup._stst_style = style return lockup from dataclasses import dataclass @dataclass class GlyphwiseGlyph(): i: int c: str e: float l: int li: int def Glyphwise2(txt:str, styler , multiline=False , tx=0 , ty=0 , start=0 , line=0) -> P: """ Experimental Glyphwise alternative; hopefully supports ligatured and RTL scripts """ if "\n" in txt: txt = txt.split("\n") if multiline and isinstance(txt, str): txt = [txt] if not isinstance(txt, str): count = 0 out = P() for lidx, line in enumerate(txt): res = Glyphwise2(line, styler, tx=tx, ty=ty, start=count, line=lidx) count += len(res) out.append(res) out.stack(10) return out g0 = GlyphwiseGlyph(-1, None, 0, line, 0) initial = StSt(txt, styler(g0)) glyphs = [p.data("glyphName") for p in initial] out = P() mods = P() for idx, glyph in enumerate(glyphs): styling = styler(GlyphwiseGlyph(start+idx, glyph, idx/len(glyphs), line, idx)) if isinstance(styling, Style): res = StSt(txt, styling)[idx] out.append(res.zero()) else: res1 = StSt(txt, styling[0])[idx] res2 = StSt(txt, styling[0].mod(**styling[1]))[idx] out.append(res1.zero()) mods.append(res2.zero()) out.spread(0) if len(mods) > 0: for idx, o in enumerate(out): out[idx] = mods[idx].align(o, tx=tx, ty=ty).data(frame=o.ambit(tx=0)) return out # def Glyphwise(st:str # , styler:Callable[[GlyphwiseGlyph], Style | list[Style | dict[str, Any]]] # , start:int=0 # , line:int=0 # , multiline=False) -> P: def Glyphwise(st:str , styler , start:int=0 , line:int=0 , multiline=False) -> P: """ Build text by applying unique style to each glyph. Style is determined by a `styler` function (usually a lambda) that is given a `GlyphwiseGlyph` containing information about the glyph and its position (index, etc.); styler function must return a Style object to be used for styling #### Examples: ```python @renderable() def glyphwise(r): return (Glyphwise("COLDTYPE", lambda x: Style(Font.ColdObvi(), 200, wdth=x.e)) .align(r)) ``` """ # TODO possible to have an implementation # aware of a non-1-to-1 mapping of characters # to glyphs? seems very difficult if not impossible, # since it requires mapping the string to glyphs # and then somehow mapping the glyphs back to the # equivalent string (?) in order to get the proper # kerning information for the sub-strings — # it maybe possible to pass glyph-id's directly # to harfbuzz, which would solve the problem, # though that does seem kind of hard to believe #glyphs = StyledString(st, styler(0, st[0])).glyphs #print(glyphs) #print([g.name for g in glyphs]) def except_reverse(): raise Exception("r=1 not possible in a Glyphwise; please use a .reversePens() after the Glyphwise constructor") def run_styler(g): styles = styler(g) if isinstance(styles, Style): if styles.reverse: except_reverse() return styles, None else: if isinstance(styles[1], dict): if styles[0].reverse: except_reverse() return styles[0], styles[0].mod(**styles[1]) else: if styles[0].reverse: except_reverse() return styles if len(st) == 1: return StSt(st, run_styler( GlyphwiseGlyph(0, st, 0, line, 0))[0], strip=False) try: lines = st.split("\n") if len(lines) > 1 or multiline: gs = [] start = 0 for lidx, l in enumerate(lines): gs.append(Glyphwise(l, styler, start=start, line=lidx)) start += len(l) return P(gs).stack() except AttributeError: pass def kx(dps, idx): return dps[idx].ambit().x def krn(off, on, idx): return kx(off, idx) - kx(on, idx) dps = P() prev = 0 tracks = [] for idx, c in enumerate(st): #c = gi.name test = c target = 0 tcount = 1 if idx < len(st) - 1: test = [test, st[idx+1]] tcount += 1 if idx > 0: test = [st[idx-1], test] target = 1 tcount += 1 if isinstance(test, str): test = [test] e = idx / (len(st)-1) gg = GlyphwiseGlyph(idx+start, c, e, line, idx) skon, skon_tweak = run_styler(gg) skoff = skon.mod(kern=0, kern_pairs={}, kp={}, tu=0) #test_list = [t for sublist in test for t in sublist] #print(test_list) test_str = "".join([t if isinstance(t, str) else "".join(t) for t in test]) #print(c, test_str, target, test, tcount) tkon = StSt(test_str, skon.mod(no_shapes=True), strip=False) if skon_tweak is None: tkoff = StSt(test_str, skoff, strip=False) tkoff_tweak = None else: tkoff = StSt(test_str, skoff.mod(no_shapes=True), strip=False) skoff_tweak = skon_tweak.mod(kern=0, kern_pairs={}, kp={}, tu=0) tkoff_tweak = StSt(test_str, skoff_tweak, strip=False) if idx == 0: if tkoff_tweak: tkoff_frame = tkoff[0].data("frame") tx = skon_tweak.input["kwargs"].get("tx", 0) ty = skon_tweak.input["kwargs"].get("ty", 0) tkoff_glyph = tkoff_tweak[0].align(tkoff_frame, tx=tx, ty=ty) tkoff_glyph.data(frame=tkoff_frame) else: tkoff_glyph = tkoff[0]#.copy(with_data=True) dps.append(tkoff_glyph) prev = krn(tkoff, tkon, 1) if target > 0: _prev = krn(tkoff, tkon, 1) prev_av = (prev+_prev)/2 if tkoff_tweak: tkoff_frame = tkoff[1].data("frame") tx = skon_tweak.input["kwargs"].get("tx", 0) ty = skon_tweak.input["kwargs"].get("ty", 0) tkoff_glyph = tkoff_tweak[1].align(tkoff_frame, tx=tx, ty=ty) tkoff_glyph.data(frame=tkoff_frame) else: tkoff_glyph = tkoff[1].copy(with_data=True) dps.append(tkoff_glyph.translate(-kx(tkoff, 1), 0)) tracks.append(-prev_av) if tcount > 2: #print("_PREV", _prev) _next = krn(tkoff, tkon, 2) - _prev #tkoff[0].ambit().w #print("next", _next) else: _next = 0 prev = _next #print(tracks) return dps.distribute( tracks=tracks ) ================================================ FILE: packages/coldtype-core/src/coldtype/text/font.py ================================================ import os, re, tempfile, sys from pathlib import Path from functools import lru_cache, cached_property from urllib.request import urlretrieve from dataclasses import dataclass from coldtype.osutil import on_linux, on_mac, on_windows, run_with_check from fontgoggles.misc.platform import setUseCocoa setUseCocoa(False) import fontgoggles.misc.hbShape as hbShape try: hbShape.CLUSTER_LEVEL = hbShape.hb.BufferClusterLevel.DEFAULT except: print("! could not set hbShape.CLUSTER_LEVEL") pass from fontgoggles.font import getOpener from fontgoggles.font.baseFont import BaseFont BLACKRENDER_ALL = False try: from blackrenderer.font import BlackRendererFont #from blackrenderer.backends.pathCollector import PathCollectorSurface, PathCollectorRecordingPen from coldtype.text.colr.brsurface import BRPathCollectorSurface, BRPathCollectorRecordingPen except ImportError: BlackRendererFont = None pass _prefixes = [ ["¬", "~/Library/Fonts"], ["", "/Library/Fonts"] ] ALL_FONT_DIRS = [] if on_mac(): ALL_FONT_DIRS = [ ".", "/System/Library/Fonts", "/Library/Fonts", "~/Library/Fonts", ] elif on_windows(): ALL_FONT_DIRS = [ ".", "C:/Windows/Fonts", ] localappdata = os.environ.get("LOCALAPPDATA") if localappdata: ALL_FONT_DIRS.append(str(Path(localappdata) / "Microsoft/Windows/Fonts/")) elif on_linux(): ALL_FONT_DIRS = [ ".", "/usr/share/fonts", "~/.local/share/fonts", "~/.fonts", ] pass FONT_FIND_DEPTH = int(os.environ.get("COLDTYPE_FONT_FIND_DEPTH", 3)) class FontNotFoundException(Exception): pass def normalize_font_prefix(path_string): for prefix, expansion in _prefixes: path_string = path_string.replace(prefix, expansion) return Path(path_string).expanduser().resolve() def normalize_font_path(font, nonexist_ok=False): global _prefixes literal = normalize_font_prefix(str(font)) ufo = literal.suffix == ".ufo" if nonexist_ok: return str(literal) if literal.exists() and (not literal.is_dir() or ufo): return str(literal) else: raise FontNotFoundException(literal) FontCache = {} FontmakeCache = {} @dataclass class FontMetrics: _cap:float = 750 _asc:float = 1000 _dsc:float = -250 upem:float = 1000 @property def cap(self): return self._cap if self._cap > 0 else self._asc if self._asc > 0 else 750 @property def asc(self): return self._asc if self._asc > 0 else self._cap if self._cap > 0 else 750 @property def dsc(self): return self._dsc if self._dsc < 0 else -250 class Font(): # TODO support glyphs? def __init__(self, path, number=0, cacheable=False, suffix=None, delete_tmp=False, black=False, system_name=None, ): tmp = None if isinstance(path, str) and path.startswith("http"): url = Path(path) sfx = url.suffix if not sfx: sfx = suffix with tempfile.NamedTemporaryFile(prefix="coldtype_download_temp", suffix="."+sfx, delete=False) as tmp: urlretrieve(path, tmp.name) path = tmp.name tmp = tmp self.path = Path(normalize_font_path(path)) numFonts, opener, getSortInfo = getOpener(self.path) self.font:BaseFont = opener(self.path, number) self.font.cocoa = False self.cacheable = cacheable self._loaded = False self.load() self.system_name = system_name self._colr = self.font.ttFont.get("COLR") self._colrv1 = (self._colr is not None #and self._colr.version == 1 and BlackRendererFont is not None) if self._colrv1 or BLACKRENDER_ALL or black: self._brFont = BlackRendererFont(self.path, fontNumber=number) else: self._brFont = None self._variations = self.font.ttFont.get("fvar") self._instances = None if tmp and delete_tmp: os.unlink(tmp.name) def __repr__(self): return f"" def load(self): if self._loaded: return self else: from coldtype.helpers import run_coroutine_sync run_coroutine_sync(self.font.load(sys.stderr.write)) self._loaded = True return self def variations(self): axes = {} if self._variations: fvar = self._variations for axis in fvar.axes: axes[axis.axisTag] = (axis.__dict__) return axes def features(self): return {*self.font.featuresGPOS, *self.font.featuresGSUB} @cached_property def metrics(self) -> FontMetrics: upem = self.font.ttFont["head"].unitsPerEm try: if "OS/2" in self.font.ttFont: os2 = self.font.ttFont["OS/2"] return FontMetrics( os2.sCapHeight if hasattr(os2, "sCapHeight") else 750, os2.sTypoAscender if hasattr(os2, "sTypoAscender") else 750, -os2.sTypoDescender if hasattr(os2, "sTypoDescender") else 250, upem) # TODO does this every happen? elif hasattr(self.font, "info"): return FontMetrics( self.font.info.capHeight, self.font.info.ascender, -self.font.info.descender, upem) # TODO also does this ever happen? elif hasattr(self.font, "defaultInfo"): return FontMetrics( self.font.defaultInfo.capHeight, self.font.defaultInfo.ascender, -self.font.defaultInfo.descender, upem) except AttributeError: return FontMetrics(upem=upem) def instances(self, scaled=True, search:re.Pattern=None): if self._variations is None: return None if self._instances is None: self._instances = {} for x in self._variations.instances: name_id = x.subfamilyNameID name_record = self.font.ttFont["name"].getDebugName(name_id) self._instances[name_record] = x.coordinates if scaled: axes = self.variations() def scale(cs): out = {} for k, v in cs.items(): axis = axes[k] out[k] = (v - axis["minValue"]) / (axis["maxValue"] - axis["minValue"]) return out return {k:scale(v) for k, v in self._instances.items()} if search is not None: keys = self._instances.keys() matches = [key for key in keys if re.search(search, key, re.IGNORECASE)] if len(matches) > 0: return self._instances[matches[0]] else: print(f"No instance keys matching: {search} :for {self.path.stem}") return None return self._instances def filename_stem(self, respacer="-"): if self.system_name is not None: return self.system_name.replace(" ", respacer) else: try: return self.names()[0].replace(" ", respacer) except: return self.path.stem def filename(self, respacer="-"): return f"{self.filename_stem(respacer)}{self.path.suffix}" def getName(self, nameID) -> str|None: for record in self.font.ttFont['name'].names: if record.nameID == nameID: return str(record) def names(self): """ returns name, family """ try: FONT_SPECIFIER_NAME_ID = 4 FONT_SPECIFIER_FAMILY_ID = 1 name = "" family = "" def decode(rec): return str(rec) # TODO should this be necessary? try: return rec.string.decode("utf-8") except UnicodeDecodeError: return rec.string.decode("utf-16-be") for record in self.font.ttFont['name'].names: if record.nameID == FONT_SPECIFIER_NAME_ID and not name: name = decode(record) if name.endswith(" None"): name = re.sub(r"\sNone$", "", name) elif record.nameID == FONT_SPECIFIER_FAMILY_ID and not family: family = decode(record) if name and family: break return name, family except: return self.path.stem, self.path.stem def chars(self) -> list[str]: ttf = self.font.ttFont cmap = ttf["cmap"] best = cmap.getBestCmap() #for table in font["cmap"].tables: # print(f"Platform {table.platformID}, Encoding {table.platEncID}, Format {table.format}") els = [] if best: all_chars = [] cmap = ttf["cmap"] for ch, name in cmap.getBestCmap().items(): all_chars.append([chr(ch), name]) return all_chars else: symbol_cmap = ttf["cmap"].getcmap(3, 0) if symbol_cmap: all_chars = [] for codepoint, glyph_name in symbol_cmap.cmap.items(): low_byte = codepoint & 0xFF char = chr(low_byte) all_chars.append([char, glyph_name]) return all_chars def subset(self, output_path, *args, unicodes="U+0000-00FF U+2B22 U+201C U+201D U+201D", features={}): _args = [ "pyftsubset", str(self.path), f"--output-file={str(output_path)}", f"--unicodes={unicodes}", "--ignore-missing-unicodes", "--ignore-missing-glyphs", "--notdef-outline", "--notdef-glyph", ] add_features = [] subtract_features = [] for k, v in features.items(): if v: add_features.append(k) else: subtract_features.append(k) print(add_features, subtract_features) if len(add_features) > 0: _args.append("--layout-features+="+",".join(add_features)) if len(subtract_features) > 0: _args.append("--layout-features-="+",".join(subtract_features)) _args.extend(args) print(">>", _args, "<<") run_with_check(_args) return Font(str(output_path)) @staticmethod def Cacheable(path, suffix=None, delete_tmp=False, actual_path=None, number=0, system_name=None): """use actual_path to override a key path (if the actual path is the result of a networked call)""" if number > 0: if not actual_path: actual_path = path path = f"{path}_#{number}" if path not in FontCache: FontCache[path] = Font( actual_path if actual_path else path, cacheable=True, suffix=suffix, delete_tmp=delete_tmp, number=number, system_name=system_name).load() return FontCache[path] @staticmethod def GDrive(id, suffix, delete=True): dwnl = f"https://drive.google.com/uc?id={id}&export=download" return Font.Cacheable(dwnl, suffix=suffix, delete_tmp=delete) @staticmethod def UnzipURL(url, font_name, path, index=0) -> "Font": import requests, zipfile, io stem = Path(url).stem font_name_short = font_name.replace(" ", "") font_cache_key = f"DownloadedFont_{font_name_short}_{index}" if font_cache_key in FontCache: return FontCache[font_cache_key] folder = Path(f"_DownloadedFonts") folder.mkdir(exist_ok=True, parents=True) r = requests.get(url) if not r.ok: raise FontNotFoundException("URL did not resolve") z = zipfile.ZipFile(io.BytesIO(r.content)) z.extractall(folder) font_path = folder / stem / path / font_name return Font.Cacheable(font_cache_key, actual_path=font_path) # Google broke this # @staticmethod # def GoogleFont(font_name, index=0) -> "Font": # import requests, zipfile, io # font_name_short = font_name.replace(" ", "") # font_cache_key = f"GoogleFont_{font_name_short}_{index}" # if font_cache_key in FontCache: # return FontCache[font_cache_key] # url = f"https://fonts.google.com/download?family={font_name}" # folder = Path(f"_GoogleFonts/{font_name_short}") # folder.mkdir(exist_ok=True, parents=True) # r = requests.get(url) # if not r.ok: # raise FontNotFoundException("GoogleFont URL did not resolve") # z = zipfile.ZipFile(io.BytesIO(r.content)) # z.extractall(folder) # font_path = list(folder.glob("*.ttf"))[index] # return Font.Cacheable(font_cache_key, actual_path=font_path) def Download(url) -> "Font": import requests font_name = Path(url).name font_cache_key = f"Download_{font_name}" if font_cache_key in FontCache: return FontCache[font_cache_key] folder = Path(f"_DownloadedFonts") folder.mkdir(exist_ok=True, parents=True) font_path = folder / font_name r = requests.get(url) if not r.ok: raise FontNotFoundException("URL did not resolve") font_path.write_bytes(r.content) return Font.Cacheable(font_cache_key, actual_path=font_path) def _ListDir(dir, regex, regex_dir, log=False, depth=0, max_depth=FONT_FIND_DEPTH, bail=False): if dir.name in [".git", "venv", ".venv"]: return results = [] try: for p in dir.iterdir(): #if len(results) > 0: # print("SKIP") # return results[0] if p.is_dir() and depth < max_depth and p.suffix != ".ufo": try: res = Font._ListDir(p, regex, regex_dir, log, depth=depth+1, bail=bail, max_depth=max_depth) if res: results.extend(res) if bail: return [results[0]] except PermissionError: pass else: if regex_dir and not re.search(regex_dir, str(p.parent), re.IGNORECASE): continue if re.search(regex, p.name, re.IGNORECASE): if p.suffix in [".otf", ".ttf", ".ttc", ".ufo", ".woff", ".woff2"]: results.append(p) if bail: return [p] except FileNotFoundError: pass return results @lru_cache() def List(regex, regex_dir=None, log=False, font_dir=None, expand=False, max_depth=FONT_FIND_DEPTH, bail=False): results = [] if "/" in regex and regex_dir is None: regex_dir, regex = regex.rsplit("/", 1) print(">>>", regex_dir, regex) font_dirs = ALL_FONT_DIRS if font_dir is not None: font_dirs = [font_dir] for dir in font_dirs: if bail and results: return [results[0]] dir = normalize_font_prefix(dir) list_dir_results = Font._ListDir(Path(dir), regex, regex_dir, log, depth=0, max_depth=max_depth, bail=bail) results.extend(list_dir_results) results = sorted(results, key=lambda p: p.stem) if expand: return [Font.Cacheable(p, system_name=p.stem) for p in results] return results @staticmethod def ListAll(regex, log=False, font_dir=None, expand=True, max_depth=FONT_FIND_DEPTH, bail=False) -> list["Font"]: results = Font.List(regex, expand=True, max_depth=max_depth) paths = set([r.path for r in results]) library_results = Font.LibraryList(regex, expand=True) for l in library_results: if l.path not in paths: results.append(l) return results #sorted(results, key=lambda p: p.system_name) @staticmethod def Find(regex, regex_dir=None, index=0, font_dir=None, number=0, max_depth=FONT_FIND_DEPTH, bail=False): if isinstance(regex, Font): return regex parts = regex.split("@") if len(parts) > 1: index = int(parts[1]) regex = parts[0] if Path(normalize_font_prefix(regex)).expanduser().exists(): return Font.Cacheable(regex, number=number) found = Font.List(regex, regex_dir, font_dir=font_dir, max_depth=max_depth, bail=bail) if len(found) == 0: raise FontNotFoundException(regex) try: return Font.Cacheable(found[index], number=number) except IndexError: print(f"> Could not get @{index}, returning 0 instead") return Font.Cacheable(found[0], number=number) except Exception as e: #print(">", e) raise FontNotFoundException(regex) @staticmethod def LibraryList(regex, print_list=False, expand=False, copy_to=None): """pass a compiled re (i.e. re.compile to _not_ ignore case)""" regex_dir = None try: regex.match("asdf") except AttributeError: if "/" in regex: regex_dir, regex = regex.rsplit("/", 1) regex = re.compile(f".*{regex}.*", re.IGNORECASE) if on_mac(): import AppKit, CoreText fonts = [x for x in list(AppKit.NSFontManager.sharedFontManager().availableFonts()) if not x.startswith(".")] fonts = list(sorted(fonts, key=lambda x: len(x))) fonts = [x for x in fonts if re.search(regex, x)] if print_list: print("---") print("Font Matches") for f in fonts: print(" >", f) if expand or copy_to: expanded = [] for f in fonts: font = AppKit.NSFont.fontWithName_size_(f, 100) path = Path(CoreText.CTFontDescriptorCopyAttribute(font.fontDescriptor(), CoreText.kCTFontURLAttribute).path()) if not regex_dir or re.search(regex_dir, str(path)): cacheable = Font.Cacheable(path, system_name=f) expanded.append(cacheable) if copy_to: cacheable.copy_to(copy_to) return expanded return fonts else: raise Exception("Library not supported on this OS") @staticmethod def LibraryFind(regex, print_list=False) -> "Font": matches = Font.LibraryList(regex, print_list=print_list) if len(matches) > 0: if on_mac(): import AppKit, CoreText try: font = AppKit.NSFont.fontWithName_size_(matches[0], 100) path = Path(CoreText.CTFontDescriptorCopyAttribute(font.fontDescriptor(), CoreText.kCTFontURLAttribute).path()) return Font.Cacheable(path) except: print("FAILED SYSTEM LOOKUP", matches[0]) raise FontNotFoundException(regex) @staticmethod def LibraryGet(regex, directory="~/Desktop", print_list=False): found = Font.LibraryFind(regex) destination = Path(directory).expanduser().absolute() / f"{str(regex)}{found.path.suffix}" found.copy_to(destination) return Font.Cacheable(destination) @staticmethod def Fontmake(source, verbose=False, keep_overlaps=False, cli_args=[]): import tempfile from subprocess import run path = Path(source).expanduser() if not path.exists(): raise FontNotFoundException(path) mtime = path.stat().st_mtime if path.suffix == ".designspace": from fontTools.designspaceLib import DesignSpaceDocument ds = DesignSpaceDocument.fromfile(path) for source in ds.sources: mtime = max(Path(source.path).stat().st_mtime, mtime) if path in FontmakeCache: _mtime, _font = FontmakeCache[path] if _mtime == mtime: return _font print(f"fontmake compiling font {path.name}") with tempfile.NamedTemporaryFile(prefix="coldtype_fontmake_", suffix=".ttf", delete=False) as tmp: args = ["fontmake", path, "--output-path", tmp.name] if path.suffix == ".designspace": args.extend(["-o", "variable"]) if keep_overlaps: args.append("--keep-overlaps") args.extend(cli_args) output = run(args, capture_output=not verbose, check=True) print(f"/fontmake compiled font {path.name}") print(tmp.name) font = Font(tmp.name) FontmakeCache[path] = [mtime, font] # TODO creation args should also go in cache os.unlink(tmp.name) return font def RegisterDir(dir): global ALL_FONT_DIRS if dir not in ALL_FONT_DIRS: ALL_FONT_DIRS.insert(0, dir) def Normalize(font, fallback=True): if isinstance(font, Path): font = str(font) if isinstance(font, str): try: _font = Font.Find(font) _font.load() # necessary? return _font except FontNotFoundException as e: if fallback: #print("font not found:", font) return Font.RecursiveMono() else: raise e elif isinstance(font, Font): return font else: # it's a list of fonts for f in font: try: return Font.Normalize(f, fallback=False) except FontNotFoundException: pass if fallback: return Font.RecursiveMono() else: raise FontNotFoundException() def copy_to(self, path:Path, filename_literal=False, return_dst=False): from shutil import copyfile dst = Path(path).expanduser().absolute() if not filename_literal: dst.mkdir(exist_ok=True, parents=True) dst = dst / self.filename() copyfile(self.path, dst) if return_dst: return dst return self @staticmethod def ColdtypeObviously(): return Font.Cacheable(Path(__file__).parent.parent / "demo/ColdtypeObviously-VF.ttf") ColdObvi = ColdtypeObviously @staticmethod def MutatorSans(): return Font.Cacheable(Path(__file__).parent.parent / "demo/MutatorSans.ttf") MuSan = MutatorSans @staticmethod def RecursiveMono(): return Font.Cacheable(Path(__file__).parent.parent / "demo/RecMono-CasualItalic.ttf") RecMono = RecursiveMono @staticmethod def JetBrainsMono(): return Font.Cacheable(Path(__file__).parent.parent / "demo/JetBrainsMono.ttf") JBMono = JetBrainsMono ================================================ FILE: packages/coldtype-core/src/coldtype/text/reader.py ================================================ from collections import OrderedDict from functools import partial from shutil import copy2 from pathlib import Path import unicodedata, math from fontTools.misc.transform import Transform from fontTools.pens.transformPen import TransformPen from coldtype.osutil import run_with_check from coldtype.color import normalize_color, rgb from coldtype.runon.path import P from coldtype.geometry import Rect from coldtype.text.font import Font, normalize_font_path, normalize_font_prefix, FontNotFoundException, ALL_FONT_DIRS, _prefixes from typing import Union from fontgoggles.misc.platform import setUseCocoa setUseCocoa(False) from fontgoggles.misc.textInfo import TextInfo from fontgoggles.font.glyphDrawing import GlyphDrawing import uharfbuzz as hb try: from blackrenderer.font import BlackRendererFont #from blackrenderer.backends.pathCollector import PathCollectorSurface, PathCollectorRecordingPen from coldtype.text.colr.brsurface import BRPathCollectorSurface, BRPathCollectorRecordingPen except ImportError: BlackRendererFont = None pass class FittableMixin(): def textContent(self): print("textContent() not overwritten") def fit(self, width): """Use various methods (tracking, `wdth` axis, etc. — properties specified in the `Style` object) to fit a piece of text horizontally to a given `width` (warning: not very fast)""" if isinstance(width, Rect): width = width.w current_width = self.width() tries = 0 if current_width > width: # need to shrink while tries < 100000 and current_width > width: adjusted = self.shrink() if adjusted: tries += 1 current_width = self.width() else: #print(">>> TOO BIG :::", self.textContent()) return self elif current_width < width: # need to expand pass #print(">>>>>>>>>>>>>>>>>> FINAL TRIES", tries) return self class Style(): """ Class for configuring font properties #### Keyword arguments * `font`: can either be a `coldtype.text.Font` object, a `pathlib.Path`, or a plain string path * `font_size`: standard point-based font-size, expressed as integer * `tracking` (aka `tu`): set the tracking, by default **in font-source-point-size** aka as if the font-size was always 1000; this means tracking is by default done relatively rather than absolutely (aka the relative tracking will not change when you change the font_size) * `trackingMode`: set to 0 to set tracking in a classic font_size-based (defaults to 1, as described just above) * `space`: set this to override the width of the standard space character (useful when setting text on a curve and the space is getting collapsed) * `baselineShift` (aka `bs`): if an integer, shifts glyphs by that amount in y axis; if a list, shifts glyphs at corresponding index in list by that amount in y axis * `xShift` (aka `xs`): if an integer, shifts glyphs by that amount in x axis; if a list, shifts glyphs at corresponding index in list by that amount in x axis * `rotate`: rotate glyphs by degree * `reverse` (aka `r`): reverse the order of the glyphs, so that the left-most glyph is first in when vectorized via `.pens()` * `removeOverlaps` (aka `ro`): automatically use skia-pathops to remove overlaps from the glyphs (useful when using variable ttf fonts) * `lang`: set language directly, to access language-specific alternate characters/rules #### Shorthand kwargs * `kp` for `kern_pairs` — a dict of glyphName->[left,right] values in font-space * `tl` for `trackingLimit` * `bs` for `baselineShift` * `ch` for `capHeight` — a number in font-space; not specified, read from font; specified as 'x', capHeight is set to xHeight as read from font """ def RegisterShorthandPrefix(prefix, expansion): global _prefixes _prefixes.append([prefix, str(expansion)]) def __init__(self, font:Union[Font, str]=None, font_size:int=12, tracking=0, trackingMode=1, postTracking=0, kern_pairs=dict(), space=None, baselineShift=0, xShift=None, rotate=0, reverse=False, removeOverlap=False, q2c=False, lang=None, narrower=None, fallback=None, palette=0, capHeight=None, ascender=None, descender=None, metrics="c", data={}, layer=None, liga=True, kern=True, fill=rgb(0, 0.5, 1), stroke=None, strokeWidth=0, instance=None, variations=dict(), variationLimits=dict(), trackingLimit=0, scaleVariations=True, rollVariations=False, mods=None, features=dict(), increments=dict(), varyFontSize=False, preventHwid=False, fitHeight=None, meta=dict(), no_shapes=False, show_frames=False, load_font=True, # should we attempt to load the font? tag=None, # way to differentiate in __eq__ annotate=False, case=None, cluster=False, **kwargs ): self.input = locals() self.input["self"] = None if load_font: self.font = Font.Normalize(font) else: self.font = font self.meta = meta self.case = case self.cluster = cluster self.fallback = fallback self.narrower = narrower self.layer = layer self.reverse = kwargs.get("r", reverse) self.removeOverlap = kwargs.get("ro", removeOverlap) self.q2c = q2c self.rotate = rotate self.scaleVariations = kwargs.get("sv", scaleVariations) self.rollVariations = kwargs.get("rv", rollVariations) self.tag = tag self.annotate = annotate self.metrics = metrics self.capHeight = kwargs.get("ch", capHeight) self.descender = kwargs.get("dsc", descender) self.ascender = kwargs.get("asc", ascender) self.no_shapes = no_shapes self.show_frames = show_frames self.complete_metrics() if "c" in self.metrics: self._asc = self.capHeight else: self._asc = self.ascender # legacy for older code if kwargs.get("fontSize"): font_size = kwargs.get("fontSize") if fitHeight: self.fontSize = (fitHeight/self._asc)*1000 else: self.fontSize = font_size self.fontSize = max(self.fontSize, 0) self.postTracking = postTracking self.tracking = kwargs.get("t", tracking) self.kern_pairs = kwargs.get("kp", kern_pairs) self.trackingMode = trackingMode self.trackingLimit = kwargs.get("tl", trackingLimit) self.baselineShift = kwargs.get("bs", baselineShift) self.increments = increments self.space = space self.xShift = kwargs.get("xs", xShift) self.mods = mods self.palette = palette self.lang = lang self.data = data self.preventHwid = preventHwid if kwargs.get("tu"): self.trackingMode = 1 # this is the default now self.tracking = kwargs.get("tu") if not self.increments.get("tracking"): self.increments["tracking"] = 5 # TODO good? found_features = features.copy() for k, v in kwargs.items(): if k.startswith("ss") and len(k) == 4: found_features[k] = v if k in ["dlig", "swsh", "onum", "tnum", "palt", "salt", "vert"]: found_features[k] = v if k in ["slig"]: if k == 0: found_features[k] = 0 self.features = {} all_features = {**dict(kern=kern, liga=liga), **found_features} if not isinstance(self.font, str): # making sure the features exist before we set them try: gpos = self.font.font.shaper.getFeatures("GPOS") gsub = self.font.font.shaper.getFeatures("GSUB") for feature, v in all_features.items(): if feature in gpos or feature in gsub: self.features[feature] = int(v) except Exception as e: print("feature finder failed", e) self.fill = normalize_color(fill) self.stroke = normalize_color(stroke) if stroke and strokeWidth == 0: self.strokeWidth = 1 else: self.strokeWidth = strokeWidth unnormalized_variations = variations.copy() self.instance = instance self.axes = OrderedDict() self.variations = dict() self.variationLimits = dict() self.varyFontSize = varyFontSize if not load_font: return else: fvar = self.font.font.ttFont.get("fvar") if fvar is not None: for axidx, axis in enumerate(sorted(fvar.axes, key=lambda ax: ax.axisTag)): generic = f"fvar_{axidx}" self.axes[axis.axisTag] = axis self.variations[axis.axisTag] = axis.defaultValue if axis.axisTag == "wdth": # the only reasonable default self.variationLimits[axis.axisTag] = axis.minValue if axis.axisTag in kwargs and axis.axisTag not in variations: v = kwargs[axis.axisTag] if v is not None: unnormalized_variations[axis.axisTag] = kwargs[axis.axisTag] if generic in kwargs and axis.axisTag not in variations: unnormalized_variations[axis.axisTag] = kwargs[generic] if self.instance: self.scaleVariations = False xs = self.font.instances(scaled=False, search=self.instance) if xs: self.addVariations(xs) else: self.addVariations(unnormalized_variations) else: self.addVariations(unnormalized_variations) def __repr__(self): if self.tag: return f"" else: return f"" def __eq__(self, other): try: if not self.tag == other.tag: return False if not self.font == other.font: return False elif not self.fontSize == other.fontSize: return False elif not self.variations == other.variations: return False except: return False for key, value in self.variations.items(): if self.variations[key] != other.variations[key]: return False if self.rotate != other.rotate: return False if self.fill != other.fill: return False return True def mod(self, **kwargs): """Modify this style object to create a new one; kwargs can have all of the same kwargs as the standard `Style` constructor""" keyed = dict(**self.input, **self.input["kwargs"]) del keyed["kwargs"] del keyed["self"] keyed.update(kwargs) return Style(**keyed) def print(self, *args): print(*args) return self def complete_metrics(self): c = False a = False d = False if self.capHeight is None and "c" in self.metrics: c = True elif self.ascender is None and "a" in self.metrics: a = True if self.descender is None and "d" in self.metrics: d = True try: if "OS/2" in self.font.font.ttFont: os2 = self.font.font.ttFont["OS/2"] if c: self.capHeight = os2.sCapHeight if hasattr(os2, "sCapHeight") else 0 if self.capHeight == 0: self.capHeight = os2.sTypoAscender if hasattr(os2, "sTypoAscender") else 750 if a: self.ascender = os2.sTypoAscender if hasattr(os2, "sTypoAscender") else 0 if self.ascender == 0: self.ascender = os2.sCapHeight if hasattr(os2, "sCapHeight") else 750 if d: self.descender = -os2.sTypoDescender if hasattr(os2, "sTypoDescender") else 250 # TODO does this every happen? elif hasattr(self.font.font, "info"): if c: self.capHeight = self.font.font.info.capHeight if a: self.ascender = self.font.font.info.ascender if d: self.descender = -self.font.font.info.descender # TODO also does this ever happen? elif hasattr(self.font.font, "defaultInfo"): if c: self.capHeight = self.font.font.defaultInfo.capHeight if a: self.ascender = self.font.font.defaultInfo.ascender if d: self.descender = -self.font.font.defaultInfo.descender except AttributeError: pass def addVariations(self, variations, limits=dict()): for k, v in self.normalizeVariations(variations).items(): self.variations[k] = v for k, v in self.normalizeVariations(limits).items(): self.variationLimits[k] = v def normalizeVariations(self, variations): scale = self.scaleVariations roll = self.rollVariations for k, v in variations.items(): try: axis = self.axes[k] except KeyError: continue if v == "min": variations[k] = axis.minValue elif v == "max": variations[k] = axis.maxValue elif v == "default": variations[k] = axis.defaultValue elif isinstance(v, str): coords = self.font.instances(scaled=False, search=v) if coords: variations[k] = coords[axis.axisTag] else: variations[k] = axis.defaultValue elif scale: vv = v if roll: vv = v%2 if 1 < vv < 2: vv = 2 - vv _v = max(0, min(1, vv)) variations[k] = float(abs(axis.maxValue-axis.minValue)*_v + axis.minValue) else: if v < axis.minValue or v > axis.maxValue: variations[k] = max(axis.minValue, min(axis.maxValue, v)) print("----------------------") print("Invalid Font Variation") print(self.font.path, self.axes[k].axisTag, v) print("> setting", v, "", variations[k]) print("----------------------") else: variations[k] = v return variations def StretchX(flatten=10, debug=0, **kwargs): d = {} def stretcher(w, xp, i, p): np = (p.flatten(flatten) if flatten else p).nonlinear_transform(lambda x,y: (x if x < xp else x + w, y)) if debug: (np.record(P() .line([(xp, -250), (xp, 1000)]) .outline())) return np def is_left(a, b, c): return ((b[0] - a[0])*(c[1] - a[1]) - (b[1] - a[1])*(c[0] - a[0])) > 0 def stretcher_slnt(w, xy, angle, i, p): if abs(angle) in [90, 270]: return p x0, y0 = xy ra = math.radians(90+angle) xdsc = x0 + (-250 - y0) / math.tan(ra) xasc = x0 + (1000 - y0) / math.tan(ra) np = (p.flatten(flatten) if flatten else p).nonlinear_transform(lambda x,y: (x if is_left((xdsc, -250), (xasc, 1000), (x, y)) else x + w, y)) if debug: (np .record(P() .line([(xdsc, -250), (xasc, 1000)]) .outline()) .record(P() .moveTo((x0+50/2, y0+50/2)) .dots(radius=50))) return np for k, v in kwargs.items(): if len(v) == 3: d[k] = (v[0], partial(stretcher_slnt, v[0], v[1], v[2])) elif len(v) == 2: d[k] = (v[0], partial(stretcher, v[0], v[1])) elif len(v) == 1: pass return d def StretchY(flatten=10, align="mdy", debug=0, **kwargs): d = {} def stretcher(h, yp, i, p): np = (p.flatten(flatten) if flatten else p).nonlinear_transform(lambda x,y: (x, y if y < yp else y + h)) if align == "mdy": np.translate(0, -h/2) elif align == "mxy": np.translate(0, -h) if debug: (np.record(P() .line([(0, yp), (p.ambit().point("E").x, yp)]) .outline())) return np def is_left(a, b, c): return ((b[0] - a[0])*(c[1] - a[1]) - (b[1] - a[1])*(c[0] - a[0])) > 0 def stretcher_slnt(h, xy, angle, i, p): if abs(angle) in [90, 270]: return p x0, y0 = xy ra = math.radians(90+angle) ydsc = y0 + (0 - x0) / math.tan(ra) yasc = y0 + (p.ambit().point("E").x - x0) / math.tan(ra) p0 = (0, ydsc) p1 = (p.ambit().point("E").x, yasc) np = (p.flatten(flatten) if flatten else p).nonlinear_transform(lambda x,y: (x, y if not is_left(p0, p1, (x, y)) else y + h)) if debug: (np .record(P() .line([p0, p1]) .outline()) .record(P() .moveTo((x0+50/2, y0+50/2)) .dots(radius=50))) return np for k, v in kwargs.items(): if len(v) == 3: d[k] = (0, partial(stretcher_slnt, v[0], v[1], v[2])) elif len(v) == 2: d[k] = (0, partial(stretcher, v[0], v[1])) elif len(v) == 1: pass return d def offset(x, y, ox, oy): return (x + ox, y + oy) class StyledString(FittableMixin): """ Lowest-level vectorized typesetting class """ def __init__(self, text:str, style:Style): if style.case is not None: if style.case == "upper": text = text.upper() elif style.case == "lower": text = text.lower() self.text_info = TextInfo(text) self.text = text self.setStyle(style) if self.style.lang: self.text_info.languageOverride = self.style.lang self.resetGlyphRun() def __repr__(self): return f"" def setStyle(self, style): self.style = style # these will change based on fitting, so we make copies self.fontSize = self.style.fontSize self.tracking = self.style.tracking self.features = self.style.features.copy() self.variations = self.style.variations.copy() def resetGlyphRun(self): self.glyphs = self.style.font.font.getGlyphRunFromTextInfo(self.text_info , features=self.features , varLocation=self.variations ) #self.glyphs = self.style.font.font.getGlyphRun(self.text, features=self.features, varLocation=self.variations) x = 0 for glyph in self.glyphs: #print(">>>>", glyph.gid) glyph.frame = Rect(x+glyph.dx, glyph.dy, glyph.ax, self.style._asc) if "d" in self.style.metrics: glyph.frame = glyph.frame.expand(self.style.descender, "N") x += glyph.ax #print("resetGlyphRun") self.getGlyphFrames() def trackFrames(self, space_width=0): t = self.tracking x_off = 0 for idx, g in enumerate(self.glyphs): g.frame = g.frame.offset(x_off, 0) x_off += t if self.style.mods: mod_data = self.style.mods.get(g.name, self.style.mods.get("_wildcard")) if mod_data is not None: x_off += mod_data[0] if self.style.space and g.name.lower() == "space": x_off += (self.style.space - space_width) def adjustFramesForPath(self): for idx, g in enumerate(self.glyphs): if self.style.xShift: try: g.frame.x += self.style.xShift[idx] except: g.frame.x += self.style.xShift pass def getGlyphFrames(self): if self.style.kern_pairs: last_gn = None for idx, glyph in enumerate(self.glyphs): gn = glyph.name for chars, kp in self.style.kern_pairs.items(): try: l = kp[0] adv = kp[1] except TypeError: l = kp adv = 0 if isinstance(chars, str): a, b = chars.split("/") else: a, b = chars if gn == b and last_gn == a: kern_shift = l if kern_shift != 0: for glyph in self.glyphs[idx:]: glyph.frame.x += kern_shift last_gn = gn space_width = 0 for glyph in self.glyphs: if glyph.name == "space" and self.style.space and self.style.space > 0: space_width = glyph.frame.w glyph.frame.w = self.style.space if self.style.trackingMode == 1: self.trackFrames(space_width=space_width) for glyph in self.glyphs: glyph.frame = glyph.frame.scale(self.scale()) if self.style.trackingMode == 0: self.trackFrames() self.adjustFramesForPath() def scale(self): return self.fontSize / self.style.font.font.shaper.face.upem def width(self): # size? try: w = self.glyphs[-1].frame.point("SE").x # TODO need to scale? except IndexError: return 0 #return w * self.scale() return w return self.getGlyphFrames()[-1].frame.point("SE").x def height(self): asc = self.style._asc * self.scale() if "d" in self.style.metrics: asc += self.style.descender * self.scale() return asc def textContent(self): return self.text def fitField(self, field, value): if field == "tracking": self.tracking = value elif field == "wdth": self.variations["wdth"] = value elif field == "fontSize": self.fontSize = value def binaryFit(self, width, field, minv, maxv, tries): midv = (maxv-minv)*0.5+minv self.fitField(field, maxv) self.resetGlyphRun() maxw = self.width() self.fitField(field, midv) self.resetGlyphRun() midw = self.width() self.fitField(field, minv) self.resetGlyphRun() minw = self.width() if abs(maxw - midw) < 0.5: #print(self.text, ">>>", tries) return if width > midw: self.binaryFit(width, field, midv, maxv, tries+1) else: self.binaryFit(width, field, minv, midv, tries+1) #return super().fit(width) def testWidth(self, width, field, minv, maxv): self.resetGlyphRun() w = self.width() if w == width: print("VERY RARE") return True elif w < width: # too small, which means we know it'll fit based on this property self.binaryFit(width, field, minv, maxv, 0) return True else: # too big, so we maintain current value & let the caller know return False def _fit(self, width): if isinstance(width, Rect): width = width.w continuing = True failed = False if self.style.tracking > 0: self.tracking = 0 if self.testWidth(width, "tracking", 0, self.style.tracking): continuing = False if continuing: minwdth = self.style.variationLimits.get("wdth", 1) currentwdth = self.style.variations.get("wdth", 1) self.variations["wdth"] = minwdth if not self.testWidth(width, "wdth", minwdth, currentwdth): #self.tracking = self.style.trackingLimit if not self.testWidth(width, "tracking", self.style.trackingLimit, min(self.style.tracking, 0)): if self.style.varyFontSize: self.fontSize = 10 if not self.testWidth(width, "fontSize", 10, self.style.fontSize): self.variations["wdth"] = minwdth failed = True else: self.variations["wdth"] = minwdth failed = True if failed: print("FAILED TO FIT >>>", self.text, self.width(), width) return self def shrink(self): #print(self.text, self.variations) adjusted = False default_step = 1 if self.tracking > 0 and self.tracking > self.style.trackingLimit: self.tracking -= self.style.increments.get("tracking", default_step) adjusted = True else: for k, v in self.style.variationLimits.items(): if self.variations[k] > self.style.variationLimits[k]: self.variations[k] -= self.style.increments.get(k, default_step) adjusted = True break if not adjusted and self.tracking > self.style.trackingLimit: self.tracking -= self.style.increments.get("tracking", default_step) adjusted = True if not adjusted and self.style.varyFontSize: self.fontSize -= 1 self.tracking = self.style.tracking adjusted = True if not adjusted and self.style.narrower: self.setStyle(self.style.narrower) adjusted = True if not adjusted and self.style.preventHwid == False and "hwid" not in self.features: self.features["hwid"] = True self.tracking = self.style.tracking # reset to widest self.resetGlyphRun() #self.glyphs = self.hb.glyphs(self.variations, self.features) adjusted = True self.resetGlyphRun() return adjusted def scalePenToStyle(self, glyph, in_pen, idx): s = self.scale() t = Transform() try: bs = self.style.baselineShift[idx] except: bs = self.style.baselineShift if callable(bs): t = t.translate(0, bs(idx)) else: try: t = t.translate(0, bs[idx]) except: try: t = t.translate(0, bs) except: pass t = t.scale(s) s = self.scale() if s > 0: t = t.translate(glyph.frame.x/s, glyph.frame.y/s) out_pen = P() tp = TransformPen(out_pen, (t[0], t[1], t[2], t[3], t[4], t[5])) ip = P().record(in_pen) if self.style.mods: mod_data = self.style.mods.get(glyph.name) if mod_data is None and "_wildcard" in self.style.mods: mod_data = self.style.mods["_wildcard"] if mod_data is not None: w, mod = mod_data mod(-1, ip) ip.replay(tp) if self.style.rotate: out_pen.rotate(self.style.rotate) # TODO this shouldn't be necessary # if True: # valid_values = [] # for (move, pts) in out_pen.value: # if move != "addComponent": # valid_values.append((move, pts)) # out_pen.value = valid_values return out_pen def _emptyPenWithAttrs(self): #attrs = dict(fill=self.style.fill) #if self.style.stroke: # attrs["stroke"] = dict(color=self.style.stroke, weight=self.style.strokeWidth) dp = P().f(self.style.fill) if self.style.stroke: dp.s(self.style.stroke).sw(self.style.strokeWidth) return dp def buildLayeredGlyph(self, idx, glyph, output, layer, frame): layerGlyph = P().record(layer) if layerGlyph.v.value: output.append(layerGlyph) layerGlyph.data(glyphName=f"{glyph.name}_layer_{idx}") #print(">>>>>>>>>>>>", layer.method) if layer.method == "drawPathSolid": layerGlyph.f(layer.data["color"]) else: gradientGlyph = P() if layer.method == "drawPathLinearGradient": (gradientGlyph .line([layer.data["pt1"], layer.data["pt2"]]) .fssw(-1, 0, 2) #.translate(frame.x, 0) ) elif layer.method == "drawPathSweepGradient": gradientGlyph.moveTo(layer.data["center"]) elif layer.method == "drawPathRadialGradient": gradientGlyph.line([layer.data["startCenter"], layer.data["endCenter"]]) else: print(">", layer.method) gradientGlyph.rect(frame) (layerGlyph .f(-1) .attr(COLR=[layer.method, layer.data]) .data(substructure=gradientGlyph)) def addBRGlyphDrawings(self, glyphs): ax = 0 surface = BRPathCollectorSurface() self.style.font._brFont.setLocation(self.variations) if self.style.palette: p = self.style.palette if isinstance(p, int): palette = self.style.font._brFont.getPalette(p) else: palette = p else: palette = None with surface.canvas((0, 0, 1000, 1000)) as canvas: for glyph in glyphs: frame = Rect( ax + glyph.dx, glyph.dy, glyph.ax, self.style._asc) # how does ay play in? output = P().data(glyphName=glyph.name, frame=frame) with canvas.savedState(): #canvas.translate(glyph.dx, glyph.dy) self.style.font._brFont.drawGlyph(glyph.name, canvas, palette=palette) layers = canvas.paths if isinstance(layers, BRPathCollectorRecordingPen): if layers.method == "drawPathSolid": # trad font output.record(layers).f(layers.data["color"]) layers = None else: layers = [layers] if layers: for idx, layer in enumerate(layers): self.buildLayeredGlyph(idx, glyph, output, layer, frame) canvas.paths = [] #canvas.translate(glyph.ax, glyph.ay) glyph.glyphDrawing = output #ax += glyph.ax def pens(self) -> P: """ Vectorize text into a `P`, such that each glyph (or ligature) is represented by a single `P` (or a `P` in the case of a color font, which will then nest a `P` for each layer of that color glyph) """ # Guess this has been here for years but it seems to be redundant? #self.resetGlyphRun() colrv1 = self.style.font._colrv1 brFont = self.style.font._brFont if brFont: self.addBRGlyphDrawings(self.glyphs) elif not self.style.no_shapes: pass #print("ct.getGlyphDrawings") #print(self.glyphs[0].glyphDrawing) #glyphNames = [g.name for g in self.glyphs] #glyphDrawings = list(self.style.font.font.getGlyphDrawings(glyphNames, True)) #for glyph, glyphDrawing in zip(self.glyphs, glyphDrawings): # glyph.glyphDrawing = glyphDrawing pens = P() for idx, g in enumerate(self.glyphs): # TODO this is sketchy but seems to correct # some line-spacing issues with arabic? norm_frame = g.frame norm_frame = Rect(g.frame.x, 0, g.frame.w, self.style._asc*self.scale()) dp_atom = self._emptyPenWithAttrs() if self.style.no_shapes: if callable(self.style.show_frames): dp_atom.record(P().rect(self.style.show_frames(g.frame)).outline(4)) else: dp_atom.record(P().rect(g.frame).outline(1 if self.style.show_frames is True else self.style.show_frames)) dp_atom.data( frame=norm_frame, glyphName=g.name, glyphCluster=g.cluster, #glyphID=g.gid, ) # dp_atom.typographic = True # dp_atom.addFrame(norm_frame) # dp_atom.glyphName = g.name elif not brFont and isinstance(g.glyphDrawing, GlyphDrawing): dp_atom.v.value = self.scalePenToStyle(g, g.glyphDrawing.path, idx).v.value if "d" in self.style.metrics: dp_atom.translate(0, self.style.descender*self.scale()) dp_atom.data( frame=norm_frame, glyphName=g.name, glyphCluster=g.cluster, #glyphID=g.gid, ) if self.style.show_frames: if callable(self.style.show_frames): #dp_atom.record(P().rect(self.style.show_frames(g.frame)).outline(4)) dp_atom.rect(self.style.show_frames(g.frame)) else: dp_atom.record(P().rect(g.frame).outline(1 if self.style.show_frames is True else self.style.show_frames)) #dp_atom.rect(g.frame) if self.style.q2c: dp_atom.q2c() if self.style.removeOverlap: dp_atom.removeOverlap() elif brFont: dp_atom = g.glyphDrawing dp_atom.layered = len(dp_atom) > 1 or colrv1 for idx, layer in enumerate(dp_atom): layer.v.value = self.scalePenToStyle(g, layer, idx).v.value ss = layer.data("substructure") if ss: ss.v.value = self.scalePenToStyle(g, ss, idx).v.value if not dp_atom.layered: dp_atom = dp_atom[0] dp_atom.data( frame=norm_frame, glyphName=g.name) if self.style.q2c: dp_atom.q2c() if self.style.removeOverlap: dp_atom.removeOverlap() else: print("HERE!") # dp_atom = P() # dp_atom.layered = True # for lidx, layer in enumerate(g.glyphDrawing.layers): # dp_layer = self._emptyPenWithAttrs() # #dp_layer.value = layer[0].value # dp_layer.v.value = self.scalePenToStyle(g, layer[0], idx).v.value # if isinstance(self.style.palette, int): # dp_layer.f(self.style.font.font.colorPalettes[self.style.palette][layer[1]]) # else: # dp_layer.f(self.style.palette[layer[1]]) # if len(dp_layer.v.value) > 0: # #dp_layer.addFrame(g.frame, typographic=True) # dp_layer.data(glyphName=f"{g.name}_layer_{lidx}") # #dp_layer.glyphName = # dp_atom += dp_layer # dp_atom.data( # frame=norm_frame, # glyphName=g.name # ) # dp_atom.addFrame(norm_frame, typographic=True) # dp_atom.glyphName = g.name #dp_atom._parent = pens if self.style.meta: dp_atom.data(**self.style.meta) pens.append(dp_atom) if self.style.cluster: def cluster(p:P): first = p[0] glyphName = "+".join([x.data("glyphName") for x in p]) for r in p[1:]: first.record(r) return first.data(glyphName=glyphName) pens = (pens .partition(lambda p: p.data("glyphCluster")) .map(cluster)) if self.style.postTracking != 0: #print(self.style.postTracking * self.style.fontSize/1000) pens.track(self.style.postTracking * self.style.fontSize/1000) if self.style.reverse: pens.reversePens() pens.data(**self.style.data) ro = pens if self.style.annotate: ro._stst = self return ro def pen(self, frame=True) -> P: """ Vectorize all text into single `P` """ return self.pens().pen() def instance(self, output_path, remove_overlaps=False, freeze=False, freeze_suffix=None): args = ["fonttools", "varLib.instancer", self.style.font.path.absolute()] for k,v in self.variations.items(): args.append(f"{k}={v}") args.extend(["-o", output_path]) if remove_overlaps: args.append("--remove-overlaps") run_with_check(args) if freeze: enabled_features = [] for k,v in self.features.items(): if v: enabled_features.append(k) features = ",".join(enabled_features) args = ["pyftfeatfreeze", "-f", features] if freeze_suffix: args.extend(["-S", "-U", freeze_suffix]) args.append(output_path) run_with_check(args) frozen_otf = Path(str(output_path) + ".featfreeze.otf") copy2(frozen_otf, output_path) frozen_otf.unlink() return Font(str(output_path)) class SegmentedString(FittableMixin): def __init__(self, text, styles): self.text_info = TextInfo(text) self.strings = [] self.segment_data = [] for segmentText, segmentScript, segmentBiDiLevel, firstCluster in self.text_info._segments: self.strings.append(StyledString(segmentText, styles[segmentScript])) cluster_data = [] for index, char in enumerate(segmentText, firstCluster): cluster_data.append( dict(index=index, char=char, unicode=f"U+{ord(char):04X}", unicodeName=unicodedata.name(char, "?"), script=segmentScript, bidiLevel=segmentBiDiLevel, dir=["LTR", "RTL"][segmentBiDiLevel % 2]) ) self.segment_data.append(cluster_data) def width(self): return sum([s.width() for s in self.strings]) def height(self): return max([s.height() for s in self.strings]) def textContent(self): return "-".join([s.textContent() for s in self.strings]) def shrink(self): adjusted = False for s in self.strings: adjusted = s.shrink() or adjusted return adjusted def pens(self, flat=True): pens = P() x_off = 0 for s in self.strings: dps = s.pens() #if dps.layered: # pens.layered = True dps.translate(x_off, 0) if flat: pens.extend(dps._els) else: if s.style.lang: dps.data(lang=s.style.lang) pens.append(dps) x_off += dps.ambit().w return pens ================================================ FILE: packages/coldtype-core/src/coldtype/text/richtext.py ================================================ import re from coldtype.runon.path import P from coldtype.text.composer import Graf, GrafStyle, Lockup from coldtype.text.reader import StyledString, Style from coldtype.color import hsl from pathlib import PurePath from typing import Optional, List, Callable, Tuple, Union from functools import reduce try: from pygments import highlight from pygments.lexers import PythonLexer from pygments.formatter import Formatter from pygments.token import Token except ImportError: highlight = None pass class RichText(P): """Very experimental module to support rich-text from annotated strings, like a super-minimal-but-open-ended subset of markdown, inspired by the way rich text is built up in the time.nle.premiere DPS subclass in coldtype""" def __init__(self, rect, text, render_text_fn:Union[dict, Callable[[str, List[str]], Tuple[str, Style]]], fit=None, graf_style=None, leading=20, tag_delimiters=["[", "]"], visible_boundaries=[" "], invisible_boundaries=[], union_styles=True, spacer="¶", strip=True, strip_lines=False): super().__init__() self.tag_delimiters = tag_delimiters self.visible_boundary_chars = visible_boundaries self.invisible_boundary_chars = invisible_boundaries self.union_styles = union_styles self.spacer = spacer if isinstance(text, PurePath): text = text.read_text() if strip: text = text.strip() if strip_lines: text = "\n".join([l.strip() for l in text.split("\n")]) self._els = self.parse_block(text, render_text_fn, rect, fit, graf_style or leading)._els def parse_block(self, txt, render_text_fn, rect, fit, graf_style): parsed_lines = [] alt_parsed_lines = [] for line in txt.splitlines(): line_meta = [] line_result = [] i = 0 in_tag = False bnd_char = "" si = 0 slugs = {0:""} metas = {0:""} bnds = {0:""} while i < len(line): if in_tag: if line[i] == self.tag_delimiters[1]: in_tag = False else: metas[si] += line[i] elif line[i] == self.tag_delimiters[0]: in_tag = True elif line[i] in self.visible_boundary_chars or line[i] in self.invisible_boundary_chars: bnd_char = line[i] bnds[si] = bnd_char si += 1 slugs[si] = "" metas[si] = "" bnds[si] = "" else: slugs[si] += line[i] i += 1 if not list(slugs.values())[-1]: line_meta = list(metas.values())[-1] last_key = list(slugs.keys())[-1] del slugs[last_key] bnds[si-1] = "" else: line_meta = [] alt_line_result = [] for i, slug in slugs.items(): b = bnds[i] if b in self.invisible_boundary_chars: b = "" styles = [] if metas[i]: styles.append(metas[i]) if line_meta: styles.append(line_meta) alt_line_result.append([slug + b, styles]) alt_parsed_lines.append(alt_line_result) parsed_lines = [] for pl in alt_parsed_lines: #print(">", pl) if pl: parsed_lines.append(pl) elif self.spacer: parsed_lines.append([[self.spacer, ["blank"]]]) lines = [] groupings = [] #from pprint import pprint #pprint(parsed_lines) for idx, line in enumerate(parsed_lines): slugs = [] texts = [] for txt, styles in line: if callable(render_text_fn): ftxt, style = render_text_fn(txt, styles) else: style = styles[0] if len(styles) > 0 else "default" ftxt = txt if style in render_text_fn: style = render_text_fn[style] else: style = render_text_fn["default"] texts.append([ftxt, idx, style, styles]) grouped_texts = [] idx = 0 done = False while not done: style = texts[idx][2] grouped_text = [texts[idx]] style_same = True while style_same: idx += 1 try: next_style = texts[idx][2] if next_style == style and self.union_styles: style_same = True grouped_text.append(texts[idx]) else: style_same = False grouped_texts.append(grouped_text) except IndexError: done = True style_same = False grouped_texts.append(grouped_text) for gt in grouped_texts: full_text = "".join([t[0] for t in gt]) styles = gt[0][3] style = gt[0][2].mod() style.data = dict(style_names=styles, txt=full_text) slugs.append(StyledString(full_text, style)) groupings.append(grouped_texts) lines.append(slugs) lockups = [] for line in lines: lockup = Lockup(line, preserveLetters=True, nestSlugs=True) if fit: lockup.fit(fit) lockups.append(lockup) graf = Graf(lockups, rect, graf_style, no_frames=True) pens = graf.pens()#.align(rect, x="minx") pens._frame = None pens.reversePens() for line in pens: line.reversePens() for slug in line: slug.reversePens() return pens.zero() def findStyle(self, style, modfn): return self.find(lambda p: style in p.data("style_names", []), modfn) def findText(self, text, modfn, flags=re.I): return self.find(lambda p: re.match(text, p.data("txt", ""), flags=flags), modfn) def removeSpacers(self, spacer=None, clean=True): if not spacer and self.spacer: spacer = self.spacer for line in self._els: txt = reduce(lambda acc, p: p.data("txt", "") + acc, line, "") if txt == spacer: line._els = [] if clean: return self.removeBlanks() return self if highlight: class ColdtypeFormatter(Formatter): def format(self, tokensource, outfile): for ttype, token in tokensource: if ttype != Token.Text: tt = re.sub(r"^Token\.", "", str(ttype)) outfile.write(f"¬{token}≤{tt}≥") else: outfile.write(token) class PythonCode(RichText): def __init__(self, rect, text, render_text_fn:Callable[[str, List[str]], Tuple[str, Style]], fit=None, graf_style=None, leading=20): if isinstance(text, PurePath): text = text.read_text() txt = highlight(text, PythonLexer(), ColdtypeFormatter()) super().__init__( rect, txt, render_text_fn, fit=fit, graf_style=graf_style or leading, tag_delimiters=["≤", "≥"], visible_boundaries=[], invisible_boundaries=["¬"]) def DefaultStyles(r, b, bi): """regular, bold, bold-italic""" return { "Keyword": (bi, hsl(0.9, s=0.6)), "Keyword.Namespace": (b, hsl(0.2, s=1)), "Name.Namespace": (b, hsl(0.55, s=1)), "Name.Builtin": (bi, hsl(0.05, s=1)), "Name.Function": (bi, hsl(0.65, s=1, l=0.7)), "Name.Decorator": (bi, hsl(0.2, 1)), "Name": (b, hsl(0.45, 0.7)), "Operator": (b, hsl(0.6, s=1)), "Operator.Word": (b, hsl(0.9)), "Punctuation": (b, hsl(0.8)), "Literal.String.Double": (r, hsl(0.15, s=0.7)), "Literal.String.Affix": (bi, hsl(0.85, s=1)), "Literal.String.Interpol": (b, hsl(0.05, s=1)), "Literal.String.Doc": (r, hsl(0.6, l=0.4)), "Literal.Number.Float": (r, hsl(0.9, s=0.7)), "Literal.Number.Integer": (r, hsl(0.45)), "Comment.Single": (r, (0.3)), "default": (r, 0.5), } ================================================ FILE: packages/coldtype-core/src/coldtype/text/shaper.py ================================================ import re import unicodedata from itertools import groupby def between(c, a, b): return ord(a) <= ord(c) <= ord(b) LATIN = lambda c: between(c, '\u0000', '\u024F') KATAKANA = lambda c: between(c, '\u30A0', '\u30FF') HIRAGANA = lambda c: between(c, '\u3040', '\u309F') CJK = lambda c: between(c, '\u4E00', '\u9FFF') modes = [ "LATIN", "ARABIC", "HEBREW", "SPACE", "CJK", "HANGUL", "KATAKANA", ] def segment(txt, mode="LATIN", includeNames=False, print_characters=False): current_mode = mode runs = [[mode]] for i, c in enumerate(txt): try: n = unicodedata.name(c) except ValueError: n = "PRIVATE" if print_characters: print(">", n) for m in modes: if m in n: if current_mode != m: current_mode = m runs.append([m]) runs[-1].append((n, c) if includeNames else c) if len(runs[0]) == 0: runs = runs[1:] for idx, (mode, *run) in enumerate(runs): p = runs[idx-1] if idx > 0 else None n = runs[idx+1] if len(runs) > idx+1 else None if p and n: if mode == "SPACE" and p[0] == n[0]: runs[idx][0] = p[0] grouped_runs = [] for k, g in groupby(runs, lambda e: e[0]): txt = "".join(["".join(g[1:]) for g in list(g)]) if txt: found_modes = set() for c in txt: try: n = unicodedata.name(c) except ValueError: n = "PRIVATE" for m in modes: if m in n: found_modes.add(m) grouped_runs.append((found_modes, txt)) # reverse number ranges in arabic for idx, (cats, line) in enumerate(grouped_runs): if "ARABIC" in cats or "HEBREW" in cats: grouped_runs[idx] = (cats, re.sub("[0-9]+", lambda m: m.group()[::-1], line)) return grouped_runs if __name__ == "__main__": from pprint import pprint s = "ABC (جاف + رطب (ما قبل" s = "(رطب (ما قبل" #s = "Ali الملخبط Boba" s = "وصل الإستيرِوflLim/Satلل" runs = segment(s, "LATIN") pprint(runs) ================================================ FILE: packages/coldtype-core/src/coldtype/timing/__init__.py ================================================ from coldtype.timing.timeable import Timeable, Easeable from coldtype.timing.timeline import Timeline class Frame(Easeable): """ Container for information about a frame Frame is the type of the first argument passed to all @animation rendering functions, usually abbreviated as `f`, i.e. .. code:: python @animation() def render(f:Frame): pass (where `Frame` is an optional type-hint if you're looking to leverage autocomplete in your editor) """ def __init__(self, i, anim, cursor=None, midi=None, recording=None): if isinstance(anim, Timeline): self.i = i % anim.duration else: self.i = i % anim.t.duration self.a = anim self.c = cursor self.m = midi self.rec = recording def adj(self, off): return Frame(self.i+off, self.a) # Easeable interface @property def t(self) -> Timeline: return self.a.t if hasattr(self.a, "t") else self.a @property def _ts(self): return None @property def autowrap(self): return True def last_render(self, modfn=lambda img: img): if not self.a.composites: raise Exception("set `composites=1` on your @animation") if self.a.last_result: return modfn(self.a.last_result) else: return None lastRender = last_render ================================================ FILE: packages/coldtype-core/src/coldtype/timing/audio.py ================================================ import math from pathlib import Path from coldtype.runon.path import P try: import numpy as np except ImportError: np = None try: import soundfile as sf except: sf = None class Wavfile(): def __init__(self, path, fps=30): p = Path(str(path)).expanduser() self.sf, self.sf_fs = sf.read(str(p)) self.fps = fps self.hz = self.sf_fs self.samples_per_frame = self.hz / fps self.path = path self.peaks = self.sf self.framelength = int(round(len(self.peaks) / self.samples_per_frame)) max_frame_amp = 0 for i in range(0, self.framelength): amp = self.amp(i) if amp > max_frame_amp: max_frame_amp = amp self.max_frame_amp = max_frame_amp def calc_peaks(self): snd = self.sf / (2.**15) s1 = snd[:, 0] return s1 def samples_for_frame(self, i): start_sample = math.floor(i * self.samples_per_frame) end_sample = math.floor(start_sample + self.samples_per_frame) return self.peaks[start_sample:end_sample] def amp(self, i): return np.average(np.fabs(self.samples_for_frame(i))) def frame_waveform(self, fi, r, inc=1, pen=None): wave = pen or P() samples = self.samples_for_frame(fi)[::inc] ww = r.w/len(samples) wh = r.h for idx, w in enumerate(samples): if idx == 0: wave.moveTo((r.x, w[0]*wh)) else: wave.lineTo((idx*ww, w[0]*wh)) wave.endPath() wave.f(None).s(1).sw(2) return wave ================================================ FILE: packages/coldtype-core/src/coldtype/timing/clip.py ================================================ import re from enum import Enum from coldtype.timing.easing import ease from coldtype.timing.timeable import Timeable class ClipType(Enum): ClearScreen = "ClearScreen" NewLine = "NewLine" GrafBreak = "GrafBreak" Blank = "Blank" Isolated = "Isolated" JoinPrev = "JoinPrev" Meta = "Meta" EndCap = "EndCap" class ClipFlags(Enum): FadeIn = "FadeIn" FadeOut = "FadeOut" class Clip(Timeable): def __init__(self, text, start, end, idx=None, track=0): self.idx = idx self.input_text = str(text) self.text = text self.start = start self.end = end self.styles = [] self.style_clips = [] self.position = 1 self.joined = False self.joinPrev = None self.joinNext = None self.track = track self.group = None self.blank = False self.blank_height = 20 self.inline_styles = [] self.inline_data = {} self.flags = {} self.type = ClipType.Isolated self.symbol = None self.symbol_position = 0 if self.text.startswith("^$"): symbol, rest = self.text.split("|") self.symbol = symbol[2:] self.symbol_position = -2 self.text = rest elif self.text.startswith("^"): symbol, rest = self.text.split("|") self.symbol = symbol[1:] self.symbol_position = -1 self.text = rest elif self.text.startswith("$"): symbol, rest = self.text.split("|") self.symbol = symbol[1:] self.symbol_position = +1 self.text = rest if self.text.startswith("*"): self.text = self.text[1:] self.type = ClipType.ClearScreen elif self.text.startswith("≈"): self.text = self.text[1:] self.type = ClipType.NewLine elif self.text.startswith("¶"): self.text = self.text[1:] self.type = ClipType.GrafBreak elif self.text.startswith("+"): self.text = self.text[1:] self.type = ClipType.JoinPrev elif self.text.startswith("µ:"): self.type = ClipType.Meta self.text = self.text[2:] elif self.text == "•": self.type = ClipType.EndCap self.text = "" parts = self.text.split(":") inline_style_marker = parts.index("ß") if "ß" in parts else -1 inline_data_marker = parts.index("∂") if "∂" in parts else -1 if inline_style_marker > -1: self.inline_styles = parts[inline_style_marker+1].split(",") if inline_data_marker > -1: qs = [q.split("=") for q in parts[inline_data_marker+1].split("&")] for k, v in qs: self.inline_data[k] = eval(v) self.text = parts[0] self.text = self.text.replace("{colon}", ":") #if "ß" in self.text: # parts = self.text.split(":") # self.text = parts[0] # self.inline_styles = parts[-1].split(",") #self.text = self.text.replace("§", "\u200b") if self.text.startswith("§"): #self.text = "\u200b" self.text = self.text[1:] #self.type = ClipType.JoinPrev self.type = ClipType.NewLine self.blank = True try: self.blank_height = float(self.text.strip()) except: pass if self.text.startswith("∫"): self.text = "" self.blank = True if not self.text: self.blank = True self.text = "" if self.text.startswith("ƒ"): r = r"^([0-9]+)ƒ" self.text = self.text[1:] value = 3 match = re.match(r, self.text) if match: value = int(match[1]) self.text = re.sub(r, "", self.text) self.flags[ClipFlags.FadeIn] = value elif self.text.endswith("ƒ"): self.flags[ClipFlags.FadeOut] = 3 self.original_text = str(self.text) def addJoin(self, clip, direction): if direction == -1: self.joinPrev = clip elif direction == 1: self.joinNext = clip def joinStart(self): if self.joinPrev: return self.joinPrev.joinStart() else: return self.start def joinEnd(self): if self.joinNext: return self.joinNext.joinEnd() else: return self.end def textForIndex(self, index): txt = self.text try: txt = self.text.split("/")[index] except: txt = self.text.split("/")[-1] return txt, self.addSpace def style_matching(self, txt): try: return next(c for c in self.style_clips if txt in c.text) except StopIteration: return None def ftext(self): txt = self.text if self.type == ClipType.Isolated: return " " + txt else: return txt def fade(self, default_fade=5): if ClipFlags.FadeIn in self.flags: return self.flags[ClipFlags.FadeIn] else: return default_fade def fadeIn(self, fi, easefn="seio", fade_length=None, start=0): if ClipFlags.FadeIn in self.flags or fade_length: if fade_length: fade = fade_length else: fade = self.flags[ClipFlags.FadeIn] if start == 0: start_frame = self.start elif start == -1: start_frame = self.start - fade if fi < start_frame: return -1 elif fi > start_frame + fade: return 1 else: fv = (fi - start_frame) / fade a, _ = ease(easefn, fv) return a return -1 def __repr__(self): return "".format([" -1", "NOW", " +1"][int(self.position)+1], int(self.start), int(self.end), self.text) ================================================ FILE: packages/coldtype-core/src/coldtype/timing/easing.py ================================================ import math import easing_functions as ef from fontTools.misc.bezierTools import splitCubic, splitLine from typing import List eases = dict( cei=ef.CubicEaseIn, ceo=ef.CubicEaseOut, ceio=ef.CubicEaseInOut, qei=ef.QuadEaseIn, qeo=ef.QuadEaseOut, qeio=ef.QuadEaseInOut, eei=ef.ExponentialEaseIn, eeo=ef.ExponentialEaseOut, eeio=ef.ExponentialEaseInOut, sei=ef.SineEaseIn, seo=ef.SineEaseOut, seio=ef.SineEaseInOut, bei=ef.BounceEaseIn, beo=ef.BounceEaseOut, beio=ef.BounceEaseInOut, eleo=ef.ElasticEaseOut, elei=ef.ElasticEaseIn, elieo=ef.ElasticEaseInOut, eleio=ef.ElasticEaseInOut, ci=ef.CubicEaseIn, co=ef.CubicEaseOut, cio=ef.CubicEaseInOut, qi=ef.QuadEaseIn, qo=ef.QuadEaseOut, qio=ef.QuadEaseInOut, ei=ef.ExponentialEaseIn, eo=ef.ExponentialEaseOut, eio=ef.ExponentialEaseInOut, si=ef.SineEaseIn, so=ef.SineEaseOut, sio=ef.SineEaseInOut, bi=ef.BounceEaseIn, bo=ef.BounceEaseOut, bio=ef.BounceEaseInOut, elo=ef.ElasticEaseOut, eli=ef.ElasticEaseIn, elio=ef.ElasticEaseInOut) all_eases = ["l", *list(eases.keys())] def curve_pos_and_speed(curve, x): x1000 = x*1000 for idx, (action, pts) in enumerate(curve.value): if action in ["moveTo", "endPath", "closePath"]: continue last_action, last_pts = curve.value[idx-1] if action == "curveTo": o = -1 a = last_pts[-1] b, c, d = pts if x1000 == a[0]: o = a[1]/1000 eb = a ec = b elif x1000 == d[0]: o = d[1]/1000 eb = c ec = d elif x1000 > a[0] and x1000 < d[0]: e, f = splitCubic(a, b, c, d, x1000, isHorizontal=False) ez, ea, eb, ec = e o = ec[1]/1000 else: continue tangent = math.degrees(math.atan2(ec[1] - eb[1], ec[0] - eb[0]) + math.pi*.5) #print(o, tangent) if tangent >= 90: t = (tangent - 90)/90 else: t = tangent/90 if o != -1: return o, t raise Exception("No curve value found!") def ease(style, x): """ Though available as a general-purpose function, this logic is usually accessed through something like the `.progress` function on an animation or timeable. Return two values — the first is the easing result at a given time x; the second is the tangent to that, if calculable (is not, atm, calculable for the mnemonics given) for reference, easing mnemonics: * cei = CubicEaseIn * ceo = CubicEaseOut * ceio = CubicEaseInOut * qei = QuadEaseIn * qeo = QuadEaseOut * qeio = QuadEaseInOut * eei = ExponentialEaseIn * eeo = ExponentialEaseOut * eeio = ExponentialEaseInOut * sei = SineEaseIn * seo = SineEaseOut * seio = SineEaseInOut * bei = BounceEaseIn * beo = BounceEaseOut * beio = BounceEaseInOut * eleo = ElasticEaseOut * elei = ElasticEaseIn, * eleio = ElasticEaseInOut """ if style == "linear" or style == "lin" or style == "l": return x, 0.5 e = eases.get(style) if e: return e().ease(x), 0.5 elif hasattr(style, "moveTo"): return style.ease_t(x), 0.5 return curve_pos_and_speed(style, x) elif type(style).__name__ == "Glyph": from coldtype.runon.path import P p = P().glyph(style) return p.ease_t(x), 0.5 elif False: if style in easer_ufo: return curve_pos_and_speed(P().glyph(easer_ufo[style]), x) else: raise Exception("No easing function with that mnemonic") else: raise Exception("No easing function with that mnemonic") def _loop(t, times=1, cyclic=True, negative=False): lt = t*times*2 ltf = math.floor(lt) ltc = math.ceil(lt) lt = lt - ltf if cyclic and ltf%2 == 1: if negative: lt = -lt else: lt = 1 - lt return lt, ltf def applyRange(e, rng): ra, rb = rng if ra > rb: e = 1 - e rb, ra = ra, rb return ra + e*(rb - ra) def ez(t, easefn="eeio", loops=0, cyclic=True, rng=(0, 1), **kwargs): t = max(0, min(1, t)) if loops > 0: t, _ = _loop(t, times=loops, cyclic=cyclic) e, _ = ease(easefn, t) if "r" in kwargs: rng = kwargs["r"] return applyRange(e, rng) def cycle(i): return all_eases[i%len(all_eases)] ================================================ FILE: packages/coldtype-core/src/coldtype/timing/midi.py ================================================ import math from collections import defaultdict from pathlib import Path from coldtype.timing.timeline import Timeline, Timeable from coldtype.timing.easing import ease, ez try: import mido except ImportError: mido = None class MidiTimeline(Timeline): def __init__(self, path, duration=None, fps=30, bpm=None, track=0, rounded=True, lookup={}, live=None ): if not mido: print("please install mido library") return def s2f(value): if rounded: return math.floor(value * fps) else: return value * fps midi_path = path if isinstance(path, Path) else Path(path).expanduser() self.midi_path = midi_path try: mid = mido.MidiFile(str(midi_path)) except FileNotFoundError: print("FILE NOT FOUND", midi_path) mid = mido.MidiFile() events = [] open_notes = [] controls = {} self.mid = mid self.time_signature = (4, 4) if fps is None: fps = 30 if bpm is None: bpm = float(mido.tempo2bpm(self.find_tempo())) for tidx, track in enumerate(mid.tracks): cumulative_time = 0 events = [] open_notes = {} for idx, msg in enumerate(track): if hasattr(msg, "note"): delta_s = mido.tick2second(msg.time, mid.ticks_per_beat, mido.bpm2tempo(bpm)) cumulative_time += delta_s o = open_notes.get(msg.note) if o != None: open_notes[msg.note] = None timeable = Timeable( s2f(o), s2f(cumulative_time), idx, name=str(msg.note), data=msg, timeline=self, track=tidx) events.append(timeable) if msg.type == "note_on" and msg.velocity > 0: open_notes[msg.note] = cumulative_time elif msg.is_cc(): delta_s = mido.tick2second(msg.time, mid.ticks_per_beat, mido.bpm2tempo(bpm)) cumulative_time += delta_s frame = s2f(cumulative_time) if msg.control not in controls: controls[msg.control] = {} controls[msg.control][frame] = msg.value self.midi_file = mid self.bpm = bpm self.fps = fps self.controls = controls if controls: max_c_frame = 0 for c, frames in controls.items(): max_c_frame = max(max_c_frame, max(frames.keys())) self._duration = max_c_frame else: try: end = sorted(events, key=lambda e: e.end)[-1].end self._duration = int(duration or end) except IndexError: self._duration = 1 try: self.min = min([int(n.name) for n in events]) self.max = max([int(n.name) for n in events]) self.notes = list(reversed(sorted(set(int(n.name) for n in events)))) self.spread = self.max - self.min except ValueError: self.min = 0 self.max = 0 self.notes = [] self.spread = 0 self.lookup = {} if lookup: self.register(lookup) super().__init__(self._duration, self.fps, timeables=events) for t in self.timeables: t.track = self.notes.index(int(t.name)) def __bool__(self): if self.midi_path.exists(): return True else: return False @property def duration(self): return self._duration def find_tempo(self): for track in self.mid.tracks: for msg in track: if msg.type == "set_tempo": return msg.tempo elif msg.type == "time_signature": self.time_signature = (msg.numerator, msg.denominator) return 500000 # 120 equivalent def register(self, lookup): for k, v in lookup.items(): self.lookup[k] = v def ki(self, key, fi=None): try: if key in self.lookup: return super().ki(self.lookup[key], fi) except TypeError: pass return super().ki(key, fi) def ci(self, control, default=0, fi=None): fi = self._norm_held_fi(fi) if control in self.controls: frames = self.controls[control] if fi in frames: return frames[fi]/127 else: _fi = fi while _fi > 0: if _fi in frames: return frames[_fi]/127 _fi -= 1 # _fi = fi # while _fi < self.duration: # if _fi in frames: # return frames[_fi]/127 # _fi += 1 return default ================================================ FILE: packages/coldtype-core/src/coldtype/timing/nle/.gitignore ================================================ test_read_ableton.xml ================================================ FILE: packages/coldtype-core/src/coldtype/timing/nle/ableton.py ================================================ from coldtype.timing import Timeline, Timeable from coldtype.helpers import sibling from pathlib import Path from functools import partial from lxml import etree import gzip, math import xml.dom.minidom as mini import xml.etree.ElementTree as ET # DeviceChain/MainSequencer/ClipTimeable/ArrangerAutomation/Events/MidiClip/Notes/KeyTracks def save_test_xml(x): sibling(__file__, "test_read_ableton.xml").write_text(etree.tostring(x, pretty_print=True).decode()) def b2f(fpb, t): return math.floor(t * fpb) def b2ms(bpm, b): return b * (60000 / bpm) def midi_to_note_name(midi_number): if not (0 <= midi_number <= 127): return "Invalid MIDI note number" note_names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] note_index = midi_number % 12 octave = (midi_number // 12) - 2 note_name = note_names[note_index] return f"{note_name}{octave}" def note_name_to_midi(note_name): note_names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] note = note_name[:-1] octave = int(note_name[-1]) if note not in note_names: return "Invalid note name" note_index = note_names.index(note) midi_number = (octave + 2) * 12 + note_index if not (0 <= midi_number <= 127): return "MIDI number out of range" return midi_number strings = { "D4": 1, "F4": 1, "G4": 1, "C4": 2, "G3": 3, "G#3": 3, "A3": 3, "B3": 3, "C3": 4, "D3": 4, "E3": 4, "F3": 4, "C2": 0, } def string_and_note_to_fret(string, note): return { 0: { "C2": 0, }, 1: { "D4": 0, "D#4": 1, "E4": 2, "F4": 3, "F#4": 4,"G4": 5, }, 2: { "C4": 0, "C#4": 1, "D4": 2, "E4": 4, }, 3: { "G3": 0, "G#3": 1, "A3": 2, "A#3": 3, "B3": 4, "C4": 5, }, 4: { "C3": 0, "C#3": 1, "D3": 2, "D#3": 3, "E3": 4, "F3": 5 } }[string][note] class AbletonMIDINote(Timeable): def __repr__(self): return f"" class AbletonMIDIClip(Timeline): def __init__(self, b2ff, clip): clip_name = clip.find("Name").attrib["Value"] clip_start = float(clip.find("CurrentStart").attrib["Value"]) clip_end = float(clip.find("CurrentEnd").attrib["Value"]) timeables_by_id = dict() super().__init__(name=clip_name, findWords=False, start=b2ff(clip_start), end=b2ff(clip_end)) for idx, kt in enumerate(clip.findall("Notes/KeyTracks/KeyTrack")): midi_key = kt.find("MidiKey").attrib["Value"] for jdx, note in enumerate(kt.find("Notes")): na = dict(note.attrib).copy() #if na["IsEnabled"]: nt = clip_start + float(na["Time"]) nd = float(na["Duration"]) note_id = na["NoteId"] vel = float(na["Velocity"]) note_name = midi_to_note_name(int(midi_key)) string = strings.get(note_name, None) if string is None: string = 0 thumb = False fret = string_and_note_to_fret(string, note_name) end = b2ff(nt+nd) if note_name == "G4" and vel < 100: end = b2ff(nt+nd+0.25) string = 5 fret = 0 thumb = True note = AbletonMIDINote(b2ff(nt), end, jdx, midi_key, id=note_id, data=dict(velocity=vel, note=note_name, string=string, fret=fret, thumb=thumb)) self.timeables.append(note) timeables_by_id[note_id] = note for idx, el in enumerate(clip.findall("Notes/PerNoteEventStore/EventLists/PerNoteEventList")): note_id = el.attrib["NoteId"] note = timeables_by_id[note_id] cc = el.attrib["CC"] if cc == "-2": adjusted_negatives = False string = note.data.get("string") start_note = int(note.name) start_note_name = midi_to_note_name(start_note) for jdx, event in enumerate(el.find("Events")): offset = float(event.attrib["TimeOffset"]) value = float(event.attrib["Value"]) if offset < 0.01 and abs(value) > 1: note.data["hammer"] = True continue if value < 0.01 and not adjusted_negatives: # pull-offs, always? if start_note_name == "D4": adjusted_negatives = True string = string + 1 note.data["string"] = string note.data["fret"] = 2 note_diff = value / 170 new_note = int(start_note) + round(note_diff) new_note_name = midi_to_note_name(new_note) #print(midi_to_note_name(start_note), string, new_note_name, value) note.timeables.append(Timeable(b2ff(offset), b2ff(offset)+1, jdx, str(new_note) , data=dict(value=value , note=new_note_name , string=string , fret=string_and_note_to_fret(string, new_note_name) ))) class AbletonMIDITrack(Timeline): def __init__(self, b2ff, track): self.b2ff = b2ff track_name = track.find("Name/EffectiveName").attrib["Value"] if track_name == "banjo": timeables = [AbletonMIDIClip(b2ff, clip) for clip in track.findall("DeviceChain/MainSequencer/ClipTimeable/ArrangerAutomation/Events/MidiClip")] else: timeables = [] super().__init__(timeables=timeables, findWords=False, name=track_name) automation = [] for a in track.xpath("AutomationEnvelopes/Envelopes/AutomationEnvelope/Automation"): automation.append(a) self.xml = track self.automation = automation def notes(self): return [int(t.name) for t in self.flat_timeables()] def range(self): ns = self.notes() return min(ns), max(ns) class AbletonAudioClip(Timeable): def __init__(self, b2ff, clip): clip_name = clip.find("Name").attrib["Value"] clip_start = float(clip.find("CurrentStart").attrib["Value"]) clip_end = float(clip.find("CurrentEnd").attrib["Value"]) super().__init__(b2ff(clip_start), b2ff(clip_end), name=clip_name) class AbletonAudioTrack(Timeline): def __init__(self, b2ff, track): self.lx = track track_name = track.find("Name/EffectiveName").attrib["Value"] clips = [] for clip in track.findall("DeviceChain/MainSequencer/Sample/ArrangerAutomation/Events/AudioClip"): clips.append(AbletonAudioClip(b2ff, clip)) automation = [] for a in track.xpath("AutomationEnvelopes/Envelopes/AutomationEnvelope/Automation"): automation.append(a) self.automation = automation super().__init__(timeables=clips, findWords=False, name=track_name) #super().__init__(clips, name=track_name) class AbletonReader(Timeline): def __init__(self, path, duration=-1, fps=30, rounded=True, note_names={}): note_names_reversed = {v:k for (k,v) in note_names.items()} als_path = path if isinstance(path, Path) else Path(path).expanduser() lx = None if als_path.suffix == ".xml": with open(str(als_path), "rb") as f: lx = etree.fromstring(f.read()) else: with gzip.open(str(als_path), "rb") as f: lx = etree.fromstring(f.read()) self.lx = lx master_track = lx.find("LiveSet/MainTrack") bpm = float(master_track.find("AutomationEnvelopes/Envelopes/AutomationEnvelope[@Id='1']/Automation/Events/FloatEvent").attrib["Value"]) fpb = (60/bpm)*fps b2ff = partial(b2f, fpb) self.b2ff = b2ff self.returns = [] tracks = [] for t in lx.xpath("LiveSet/Tracks/*"): if t.tag == "MidiTrack": tracks.append(AbletonMIDITrack(b2ff, t)) elif t.tag == "AudioTrack": #tracks.append(AbletonAudioTrack(b2ff, t)) pass elif t.tag == "ReturnTrack": self.returns.append(t) if duration == -1: duration = max([t.end for t in tracks]) # huh? #for t in tracks: # t.constrain(0, duration) super().__init__(duration=duration, fps=fps, timeables=tracks, findWords=False, name="Ableton") #super().__init__(duration, fps=fps, tracks=tracks) if __name__ == "": from coldtype import * ar = AbletonReader("~/Waves/projs/2024.02.05.1 Project/__silentnighttab3.als") #print(ar.timeables[0].timeables[0].timeables) # el = ar.timeables[1].automation[0] # points = {} # for fe in el.findall("Events/FloatEvent"): # b = float(fe.attrib["Time"]) # frame = ar.b2ff(b) # percent = float(fe.attrib["Value"]) # fr = b2f((60/120)*30, b) # if fr < 0: # fr = 0 # points[fr] = percent # interp = [] # for fr in range(0, ar.duration): # p = points.get(fr, None) # if p: # interp.append(p) # else: # pass #reparsed = mini.parseString(ET.tostring(el, 'utf-8')) #print(reparsed.toprettyxml(indent=" ")) colors = { 0: bw(1), 1: hsl(0.3, 0.60, 0.6), 2: hsl(0.6, 0.7, 0.65), 3: hsl(0.9, 0.7, 0.5), 4: hsl(0.6, 0.90, 0.58), 5: hsl(0.9, 0.7, 0.6), } string_names = { 0: "X", 1: "D", 2: "C", 3: "G", 4: "C", 5: "g", } string_notes = { "D4": 1, "C4": 2, "G3": 3, "C3": 4, "G4": 5, "C2": 0, } @animation(Rect(1080*2.5, 1080), tl=Timeline(3400, 30), bg=0, fmt="png") def ableton(f): xs = 4 ys = 32 notes = P() hits = P() frets = P() bars = P() string_labels = P() g = 1725 ascii_tab = { 1: {}, 2: {}, 3: {}, 4: {}, 5: {}, } denom = (36/6) for t in ar.timeables[0].timeables[0].timeables: n:AbletonMIDINote = t note = n.data.get("note") fret = str(n.data.get("fret")) string = n.data.get("string") vel = n.data.get("velocity") color = colors.get(string, 1) x = n.start*xs y = int(n.name)*ys if note != "C2": beat = n.start/denom ascii_tab[string][round(beat)] = fret def add_fret(_fret, x, y): frets.append(StSt(_fret, "MDIO-VF", 26, wght=0.65).t(x-9, y+32).f(0) .pen().up().insert(0, lambda p: P().oval(p.ambit(tx=0, ty=0).inset(-9)) .f(color.lighter(0.2).with_alpha(1)))) if note == "C2": y = 1620 bars.append(P().line([Point(x, y).o(0, -2), Point(x, y).o(0, 1000)]).fssw(-1, bw(1, min(0.35, vel/127)), 2)) continue show_note = True if string == 5: show_note = False y = g bend_points = [Point(x, y+ys/2)] last_y = y+ys/2 last_fret = fret for b in n.timeables: #if b.start < 0.01: # continue _x = x + b.start*xs _y = y+ys/2+(b.data["value"]/170)*ys p = Point(_x, _y) last_y = p.y bend_points.append(p) bend_fret = str(b.data.get("fret")) if b.start > 0.01 and last_fret != bend_fret: last_fret = bend_fret if bend_fret != "0": add_fret(bend_fret, _x, _y-12) beat = (n.start+b.start)/(denom) ascii_tab[string][round(beat)] = bend_fret bend_points.append(Point(x+n.duration*xs, last_y)) notes.append(P().line(bend_points).f(color).outline(5, cap="butt").ro()) a = Point(x, y).o(0, -2) b = Point(x, y).o(0, ys+2) if n.data.get("hammer"): hits.append(P().line([a, b]).rotate(-20).mirrorx().fssw(-1, 1, 2)) elif n.data.get("thumb"): hits.append(P().m(a.o(12, 0)).bxc(a.interp(0.5, b), "SW").bxc(b.o(12, 0), "NW").ep().fssw(-1, 1, 2)) else: hits.append(P().line([a, b]).fssw(-1, 1, 2)) if show_note and fret != "0": add_fret(fret, x, y) opens = P() last_string = None for x in range(note_name_to_midi("C3"), note_name_to_midi("G4")+2): note = midi_to_note_name(x) if "#" not in note: y = x*ys+ys/2 if note in string_notes: string = string_notes[note] if note != "G4": last_string = string _y = y if string == 5: _y = g + ys/2 opens.append(P( P().line([Point(-100, _y), Point(notes.ambit().mxx + 100, _y)]) .fssw(-1, colors[string].with_alpha(0.65), 3))) string_labels.append(P( StSt(string_names[string], "MDPrimer-Bl", 66, wght=0.75).t(-160, _y-16).shift(-0.5, 0).f(colors[string]))) if note not in string_notes or note == "G4": color = bw(1) opens.append(P( P().line([Point(-100, y), Point(notes.ambit().mxx + 100, y)]) .fssw(-1, colors[last_string].with_alpha(0.65), 1))) string_labels.append(P( StSt(note[0], "MDPrimer-Se", 32, wght=0.25).t(-126, y-9).f(colors[last_string]))) # ascii_out = [] # for string, beats in ascii_tab.items(): # string_line = f"{string_names[string].upper()}|" # for x in range(0, 20): # if x in beats: # string_line += beats[x] # else: # string_line += "-" # string_line += "|" # ascii_out.append(string_line) #Path("ascii.txt").write_text("\n".join(ascii_out)) return P( P( opens, P( bars, notes, hits, frets ), string_labels, ) .align(f.a.r.inset(60), "W") .index(-1, lambda p: p.insert(0, P(p.ambit().take(100, "W").inset(-44, -350).expand(100, "W")).f(bw(0)))) .index(1, lambda p: p.t(200+-f.i*2, 0)) .insert(0, P().rect(Rect(432, 0, 21, 2800)) .f(hsl(0.17, 1.00, 1.00, 0.20))) , #StSt("“Silent Night” — gCGCD tuning — 6/8, swung", "MDPrimer-Se", 40).align(f.a.r.inset(70), "NW").f(1), ) #e1 = ar.timeables[1].timeables[0].ki(60, f.i).adsr([7, 0, 0, 30]) #e2 = ar.timeables[1].timeables[0].ki(65, f.i).adsr([7, 0, 0, 30]) #return (P( # P().oval(f.a.r.inset(100)).fssw(-1, hsl(0.017, 0.8, 0.7), 3).scale(e1+0.25), # P().oval(f.a.r.inset(100)).fssw(-1, hsl(0.7, 0.8, 0.7), 3).scale(e2+0.2), #)) ================================================ FILE: packages/coldtype-core/src/coldtype/timing/nle/ascii.py ================================================ from typing import Union from coldtype.interpolation import interp_dict from coldtype.timing.timeline import Timeline, Timeable from coldtype.geometry.rect import Rect class AsciiTimeline(Timeline): __name__ = "AsciiTimeline" def __init__(self, multiplier:Union[int,float], fps:float, ascii:str=None, keyframes:dict=None, eases:dict=None, **kwargs ): if isinstance(fps, str): ascii = fps fps = 30 lines = [l.rstrip() for l in ascii.splitlines() if l.strip()] ml = max([len(l) for l in lines]) - 1 self.multiplier = multiplier self.keyframes = self._norm_keyframes(keyframes) self.eases = eases clips = [] unclosed_clip = None duration = round(multiplier*ml) for lidx, l in enumerate(lines): if l.startswith("#"): continue clip_start = None clip_name = None instant_clip = None if unclosed_clip: clip_start, clip_name = unclosed_clip unclosed_clip = None looped_clip_end = None for idx, c in enumerate(l): if c == "]": if clip_start is not None and clip_name is not None: clips.append(Timeable( clip_start, round((idx)*multiplier)+multiplier, name=clip_name, data=dict(line=lidx), track=lidx-1, timeline=self)) else: looped_clip_end = round(idx*multiplier) clip_start = None clip_name = None elif c == "[": clip_start = round(idx*multiplier) clip_name = "" elif c not in [" ", "-", "|", "<", ">"]: if clip_name is None: if instant_clip: instant_clip += c else: clip_start = round(idx*multiplier) instant_clip = c # if clip_name is None: # clips.append(Timeable( # round(idx*multiplier), # round(idx*multiplier), # name=c, # data=dict(line=lidx), # timeline=self)) else: clip_name += c elif c in [" "] and instant_clip: clips.append(Timeable( clip_start, clip_start, name=instant_clip, data=dict(line=lidx), track=lidx-1, timeline=self)) instant_clip = None if instant_clip: clips.append(Timeable( clip_start, clip_start, name=instant_clip, data=dict(line=lidx), track=lidx-1, timeline=self)) if looped_clip_end: if clip_start is not None and clip_name is not None: clips.append(Timeable( clip_start, duration+looped_clip_end, name=clip_name, data=dict(line=idx), track=lidx-1, timeline=self)) clip_start = None clip_name = None if clip_start is not None and clip_name is not None: unclosed_clip = (clip_start, clip_name) clips = sorted(clips, key=lambda c: c.start) for cidx, clip in enumerate(clips): clip.idx = cidx super().__init__(duration, fps, clips, **kwargs) def _norm_keyframes(self, keyframes): if not keyframes: return {} if not isinstance(keyframes, dict): kfs = {} for idx, v in enumerate(keyframes): kfs[str(idx)] = v return kfs else: kfs = {} for k, v in keyframes.items(): kfs[str(k)] = v return kfs def _find_kf_easer(self, ease, eases): for t in self.timeables: if t.name.startswith("~") and t.name[1:] in eases.keys(): if ease.start <= t.start < ease.end: return eases[t.name[1:]] return "eeio" def keyframe_current(self, fi, keyframes, lines=None): for c1, c2 in self.enumerate(lines=lines, pairs=True, edges=True, filter=keyframes.keys()): start, end = c1.start, c2.start if c2.start < c1.end: end += self.duration if fi < c1.start: fi += self.duration t = Timeable(start, end, name=f"_kf_{c1.name}/{c2.name}", timeline=self) if t.now(fi): return [fi, t, c1, c2] def kf(self, easefn=None, fi=None, lines=None, keyframes=None, eases=None, offset=0): fi = self._norm_held_fi(fi) if keyframes is None: keyframes = self.keyframes else: keyframes = self._norm_keyframes(keyframes) if offset: # if callable(offset): # res_ = self.keyframe_current(fi % self.duration, keyframes, lines) # if res_: # _, t, c1, c2 = res_ # fi = fi + offset(f"{c1.name}->{c2.name}") # else: # fi = fi + offset(None) # else: fi = fi + offset fi = fi % self.duration res = self.keyframe_current(fi, keyframes, lines) if res: fi, t, c1, c2 = res if c1.name == c2.name: return keyframes[c1.name] else: easer = easefn if easer is None: if eases or self.eases: easer = self._find_kf_easer(t, eases or self.eases) else: easer = "eeio" return interp_dict( t.at(fi).e(easer, 0), keyframes[c1.name], keyframes[c2.name]) return list(keyframes.values())[0] def enumerate(self, lines=None, pairs=False, edges=False, filter=None): matches = [] for c in self.timeables: match = False if c.name.startswith("~"): match = False elif filter and c.name not in filter: match = False elif lines is not None: if c.data["line"] in lines: match = True else: match = True if match: if edges and c.duration > 0: matches.append(Timeable(c.start, c.start, -1, name=c.name, timeline=self)) matches.append(Timeable(c.end-1, c.end-1, -1, name=c.name, timeline=self)) else: matches.append(c) for i, c in enumerate(matches): if pairs: if i < len(matches)-1: yield c, matches[i+1] else: yield c, matches[0] else: yield c def inflate(self, lines=None): for a, b in self.enumerate(pairs=True, lines=lines): if a.start == a.end: if a.start < b.start: a.end = b.start else: a.end = self.duration return self def rmap(self, r=Rect(1000, 1000)): """ Rect-map, i.e. a representation of this ascii timeline as a 2D map of rectangles """ from coldtype.geometry.rect import Rect out = {} for clip in self.timeables: sc = r.w / self.duration out[clip.name] = Rect(clip.start * sc, 0, clip.duration * sc, r.h) return out def __getitem__(self, item): return self._keyed(item) ================================================ FILE: packages/coldtype-core/src/coldtype/timing/nle/premiere.py ================================================ import json from pathlib import Path # import extra stuff for convenience in user code from coldtype.timing.sequence import Marker, ClipTrack, Clip, Sequence, ClipGroup, ClipGroupPens VIDEO_OFFSET = 86313 # why is this? group_pens_cache = {} def to_frames(seconds, fps): frames = int(round(float(seconds)*fps)) if frames >= VIDEO_OFFSET: frames -= VIDEO_OFFSET return frames class PremiereTimeline(Sequence): __name__ = "Premiere" def __init__(self, path, storyboard=None, duration_override=None, workarea_track=0): self.path = path json_path = path if isinstance(path, Path) else Path(path).expanduser() try: jsondata = json.loads(json_path.read_text()) except: print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") print(json_path.read_text()) raise Exception("NOPE") jsondata = {} meta = jsondata.get("metadata") fps = 1 / meta.get("frameRate") duration = int(round(int(meta.get("duration"))/int(meta.get("timebase")))) if duration_override: duration = duration_override self.start = 0 self.end = duration tof = lambda s: int(round(float(s)*fps)) cti = tof(meta.get("cti", 0)) self.cti = cti _storyboard = [] _storyboard.append(self.cti) for m in jsondata.get("storyboard"): f = tof(m.get("start")) if f not in _storyboard: _storyboard.append(f) if storyboard: _storyboard = storyboard workareas = [] workareas.append(range(max(0, tof(meta.get("inPoint"))), tof(meta.get("outPoint"))+1)) self.workareas = workareas tracks = [] for tidx, track in enumerate(jsondata.get("tracks")): markers = [] for marker in track.get("markers"): markers.append(Marker( to_frames(marker.get("start"), fps), to_frames(marker.get("end"), fps), marker)) clips = [] for cidx, clip in enumerate(track.get("clips")): clips.append(Clip( clip.get("name"), to_frames(clip.get("start"), fps), to_frames(clip.get("end"), fps), idx=cidx, track=tidx)) tracks.append(ClipTrack(self, clips, markers)) super().__init__(duration, fps, _storyboard, tracks, workarea_track=workarea_track) ================================================ FILE: packages/coldtype-core/src/coldtype/timing/sequence.py ================================================ import re, math, copy from enum import Enum from collections import namedtuple from dataclasses import dataclass from coldtype.timing import Timeable, Frame from coldtype.timing.easing import ease from coldtype.timing.timeline import Timeline from coldtype.text import StyledString, Lockup, Graf, GrafStyle, Style, Rect from coldtype.runon.path import P from coldtype.color import * from coldtype.timing.clip import Clip, ClipFlags, ClipType from typing import Optional, Union, Callable, Tuple, List class Marker(Timeable): def __init__(self, start, end, marker): # TODO super name= marker name? super().__init__(start, end, data=marker) class ClipGroupPens(P): def setClipGroup(self, clip_group): self.cg = clip_group return self def _iterate_tags(self, tag, pens): if len(pens) > 0: for pen in pens: if pen._tag == tag: yield pen.data(tag), pen else: yield from self._iterate_tags(tag, pen) def iterate_clips(self): yield from self._iterate_tags("clip", self) def iterate_slugs(self): yield from self._iterate_tags("slug", self) def iterate_lines(self): yield from self._iterate_tags("line", self) def map_clips(self, fn): for clip, pen in self.iterate_clips(): fn(clip, pen) return self def clean_empties(self): for _, pen in self.iterate_slugs(): pen._els = [p for p in pen._els if len(p._els) > 0] for line in self: line._els = [p for p in line._els if len(p._els) > 0] self._els = [p for p in self._els if len(p._els) > 0] return self def remove_futures(self, clean=True): def futureRemover(p, pos, _): if pos == 1 and "position" in p._data and "clip" in p._data: if p.data("position") > 0: p._els = [] self.walk(futureRemover) if clean: self.removeBlanks() return self for clip, pen in self.iterate_clips(): if clip.position > 0: pen._els = [] if clean: self.clean_empties() return self removeFutures = remove_futures def remove_future_lines(self, clean=True): for _, line in self.iterate_lines(): any_now = False for pen in line: for p in pen: clip = p.data("clip") if clip: if clip.position <= 0: any_now = True if not any_now: line._els = [] #for x in line.iterate_clips(): # print(">>>", x) #for clip, pen in self.iterate_clips(): # if clip.position > 0: # pen._els = [] if clean: self.clean_empties() return self @dataclass class ClipGroupTextSetter(): frame:Frame i:int clip:Clip text:str styles:Timeline class ClipGroup(Timeable): def __init__(self, timeline, index, clips, regroup=True): # if not regroup: # new_clips = [] # for idx, c in enumerate(clips): # c:Clip # nc = Clip(c.text, c.start, c.end, idx, 0) # nc.group = self # new_clips.append(nc) # clips = new_clips self.index = index self.clips = clips self.timeline = timeline self.style_indices = [] self.styles = [] self.retime() if regroup: for idx, clip in enumerate(clips): clip.idx = idx clip.group = self def retime(self): if len(self.clips) > 0: self.start = self.clips[0].start self.end = self.clips[-1].end self.track = self.clips[0].track self.valid = True else: self.start = 0 self.end = 0 self.track = None self.valid = False def styles(self): all_styles = set() for clip in self.clips: for style in clip.styles: all_styles.add(style) return all_styles def style_matching(self, style): for clip in self.clips: match = clip.style_matching(style) if match: return match def ldata(self, field, default): if len(self.clips) > 0: return self.clips[-1].inline_data.get(field, default) def current_style_matching(self, f:Frame, identifier:Union[str, Callable[[Clip], bool]]): for si in self.style_indices: sc = self.timeline.sequence[si].current(f.i) if sc and identifier(sc): if isinstance(identifier, str): if identifier in sc.text: return sc elif identifier(sc): return sc def lines(self, ignore_newlines=False): lines = [] line = [] for clip in self.clips: if clip.type == ClipType.NewLine: if ignore_newlines: if not clip.text.startswith(" "): clip.text = " " + clip.text line.append(clip) else: lines.append(line) line = [clip] elif clip.type == ClipType.GrafBreak: lines.append(line) cclip = copy.deepcopy(clip) cclip.text = "¶" lines.append([cclip]) line = [clip] # add a graf mark else: line.append(clip) if len(line) > 0: lines.append(line) return lines def position(self, idx, styles): for clip in self.clips: clip.joined = False for clip in self.clips: clip:Clip clip.styles = [] if False: if styles: for style in styles: if style: for style_clip in style.clips: if clip.start >= style_clip.start and clip.end <= style_clip.end: clip.styles.append(style_clip.ftext().strip()) elif style_clip.start >= clip.start and style_clip.end <= clip.end: clip.styles.append(style_clip.ftext().strip()) if clip.start > idx: clip.position = 1 elif clip.start <= idx and clip.end > idx: clip.position = 0 before_clip = clip while before_clip.type == ClipType.JoinPrev: before_clip = self.clips[before_clip.idx-1] before_clip.joined = True try: after_clip = self.clips[clip.idx+1] while after_clip.type == ClipType.JoinPrev: after_clip.joined = True after_clip = self.clips[after_clip.idx+1] except IndexError: pass else: clip.position = -1 if True: for style in styles: style:ClipTrack if clip.position == -1: sc = style.current(clip.end-1) elif clip.position == 0: sc = style.current(idx) else: sc = style.current(clip.start) if sc: clip.styles.extend([s.strip() for s in sc.ftext().strip().split(",")]) clip.style_clips.append(sc) return self def currentSyllable(self): for clip in self.clips: if clip.position == 0: return clip def current_word(self, fi): for clip in self.clips: if clip.start <= fi < clip.end: clips = [clip] before_clip = clip # walk back while before_clip.type == ClipType.JoinPrev: before_clip = self.clips[before_clip.idx-1] clips.insert(0, before_clip) # walk forward try: after_clip = self.clips[clip.idx+1] while after_clip.type == ClipType.JoinPrev: clips.append(after_clip) after_clip = self.clips[after_clip.idx+1] except IndexError: pass return clips currentWord = current_word def currentLine(self): for line in self.lines(): for clip in line: if clip.position == 0: return line def sibling(self, clip, direction, wrap=False): try: if clip.idx + direction < 0 and not wrap: return None elif clip.idx + direction >= len(self.clips): if not wrap: return None else: return self.clips[0] return self.clips[clip.idx + direction] except IndexError: return None def text(self): txt = "" for c in self.clips: if c.type == ClipType.ClearScreen: pass elif c.type == ClipType.Isolated: txt += "( )" elif c.type == ClipType.JoinPrev: txt += "|" elif c.type == ClipType.NewLine: txt += "/(\\n)/" txt += c.text return txt def pens(self, fi, render_clip_fn:Callable[ [ClipGroupTextSetter], Tuple[str, Style]], rect=None, graf_style=GrafStyle(leading=20), fit=None, ignore_newlines=False, use_lines=None, styles:List[Timeable]=[], removeOverlap=False) -> ClipGroupPens: """ render_clip_fn: frame, line index, clip, clip text — you must return a tuple of text to render and the Style to render it with """ if isinstance(fi, Frame): fi = fi.i if not rect: rect = Rect(1080, 1080) if not styles or len(styles) == 0: styles = self.styles group_pens = ClipGroupPens().setClipGroup(self) lines = [] groupings = [] if not use_lines or (use_lines and use_lines[0] is None): use_lines = self.lines(ignore_newlines=ignore_newlines) for idx, _line in enumerate(use_lines): if not _line: continue slugs = [] texts = [] for idx, clip in enumerate(_line): if clip.type == ClipType.Meta or clip.blank: continue try: match_styles = [] for s in styles: if s.now(clip.start): match_styles.append(s) match_styles = Timeline(timeables=match_styles, findWords=False) match_styles.hold(fi) ftext = clip.ftext() if clip.type == ClipType.Isolated and idx == 0: ftext = ftext[1:] text, style = render_clip_fn(ClipGroupTextSetter(fi, idx, clip, ftext, match_styles)) texts.append([text, clip, style]) except Exception as e: print(e) raise Exception("pens render_clip must return text & style") grouped_texts = [] idx = 0 done = False while not done: style = texts[idx][2] grouped_text = [texts[idx]] style_same = True while style_same: idx += 1 try: next_style = texts[idx][2] if next_style == style: style_same = True grouped_text.append(texts[idx]) else: style_same = False grouped_texts.append(grouped_text) except IndexError: done = True style_same = False grouped_texts.append(grouped_text) for gt in grouped_texts: full_text = "".join([t[0] for t in gt]) slugs.append(StyledString(full_text, gt[0][2])) groupings.append(grouped_texts) lines.append(slugs) lockups = [] for line in lines: lockup = Lockup(line, preserveLetters=True, nestSlugs=True) if fit: lockup.fit(fit) lockups.append(lockup) graf = Graf(lockups, rect, graf_style) pens = graf.pens()#.align(rect, x="minx") re_grouped = P() for idx, line in enumerate(lines): #print(pens, idx, line[0].text) line_dps = pens[idx] re_grouped_line = P() re_grouped_line.tag("line") position = 1 line_text = "" for gidx, gt in enumerate(groupings[idx]): group_dps = line_dps[gidx] tidx = 0 last_clip_dps = None for (text, clip, style) in gt: clip:Clip# = self.clips[cidx] if fi < clip.start: position = 1 elif fi == clip.start or fi < clip.end: position = 0 else: position = -1 # if clip.position == 0: # position = 0 # elif clip.position == -1: # position = -1 line_text += clip.ftext() clip_dps = P(group_dps[tidx:tidx+len(text)]) clip_dps.tag("clip") clip_dps.data( style=style, clip=clip, line_index=idx, line=re_grouped_line, group=re_grouped, position=position) if clip.type == ClipType.JoinPrev and last_clip_dps: grouped_clip_dps = last_clip_dps #grouped_clip_dps.append(last_clip_dps) #grouped_clip_dps._els = last_clip_dps._els grouped_clip_dps.append(clip_dps) #re_grouped_line[-1] = grouped_clip_dps #last_clip_dps = grouped_clip_dps else: last_clip_dps = P() last_clip_dps.tag("slug") last_clip_dps.append(clip_dps) re_grouped_line.append(last_clip_dps) tidx += len(text) re_grouped_line.data( line_index=idx, position=position, line_text=line_text) re_grouped.append(re_grouped_line) pens = re_grouped for pens in pens._els: if removeOverlap: for pen in pens._els: pen.removeOverlap() group_pens.append(pens) for clip, pen in group_pens.iterate_clips(): if clip.position == 1 or clip.text == "¶": #pen.f(0) pass if clip.blank: pen._els = [] for line in group_pens: line.reversePens() # line top-to-bottom for slug in line: slug.reversePens() # slugs left-to-right for clip in slug: clip.reversePens() # glyphs left-to-right #.understroke(sw=5) group_pens.reversePens() return group_pens def iterate_clip_pens(self, pens): if len(pens) > 0: for pen in pens: if hasattr(pen, "data"): if pen.data("clip"): yield pen.data("clip"), pen else: yield from self.iterate_clip_pens(pen) def remove_futures(self, pens): for clip, pen in self.iterate_clip_pens(pens): if clip.position > 0: pen._els = [] removeFutures = remove_futures def iterate_pens(self, pens, copy=True): for idx, line in enumerate(self.lines()): _pens = pens[idx] for cidx, clip in enumerate(line): if copy: p = _pens._els[cidx].copy() else: p = _pens._els[cidx] yield idx, clip, p def __repr__(self): return "".format(int(self.start), int(self.end), self.text()) def __hash__(self): return self.text() class ClipTrack(): def __init__(self, sequence, clips, markers, styles=None): self.sequence = sequence self.clips = clips self.markers = markers self.clip_groups = self.groupedClips(clips) self.styles = styles self._holding = -1 def hold(self, i): self._holding = i if self.styles is not None: self.styles.hold(i) def groupedClips(self, clips): groups = [] group = [] last_clip = None for idx, clip in enumerate(clips): if clip.type == ClipType.ClearScreen: if len(group) > 0: groups.append(ClipGroup(self, len(groups), group)) group = [] elif clip.type == ClipType.JoinPrev: if last_clip: last_clip.addJoin(clip, +1) clip.addJoin(last_clip, -1) group.append(clip) last_clip = clip if len(group) > 0: groups.append(ClipGroup(self, len(groups), group)) return groups def current(self, fi): for idx, clip in enumerate(self.clips): clip:Clip if clip.start <= fi and fi < clip.end: return clip def _norm_held_fi(self, fi=None): """ClipTrack should be a Timeline, this should be unnecessary""" if fi is None: if self._holding >= 0: return self._holding else: raise Exception("Must provide fi if ClipTrack is not held") return fi def currentGroup(self, fi=None) -> ClipGroup: """modern""" fi = self._norm_held_fi(fi) for cg in self.clip_groups: cg:ClipGroup if cg.start <= fi and fi < cg.end: cg.styles = self.styles return cg return ClipGroup(self.sequence, -1, []) def currentWord(self, fi=None) -> ClipGroup: """modern""" fi = self._norm_held_fi(fi) cg = self.currentGroup(fi) if cg: cw = cg.currentWord(fi) return ClipGroup(self.sequence, cg.index, cw or [], regroup=False) else: return ClipGroup(self.sequence -1, []) def now(self, fi, text): for idx, clip in enumerate(self.clips): clip:Clip if clip.text == text and clip.start <= fi and fi < clip.end: return clip return False def now_or_past(self, fi, text): for idx, clip in enumerate(self.clips): clip:Clip if clip.text == text and clip.start <= fi: return clip return False def over(self, fi, text): for idx, clip in enumerate(self.clips): clip:Clip if clip.text == text and fi >= clip.end: return True return False def duration(self): duration = 0 for c in self.clips: duration = max(duration, c.end) return duration def __repr__(self): return "".format("/".join([c.ftext() for c in self.clips[:3]])) class Sequence(Timeline): def __init__(self, duration, fps, storyboard, tracks, workarea_track=0): self.workarea_track = workarea_track super().__init__(duration, fps, storyboard, tracks) def find_symbol(self, symbol): start = -1 end = -1 for t in self.tracks: for c in t.clips: c:Clip if c.symbol and c.symbol == symbol: if c.symbol_position == -2: start = c.start end = c.end elif c.symbol_position == -1: start = c.start elif c.symbol_position == +1: end = c.end return start, end def retime_for_symbol(self, symbol): start, end = self.find_symbol(symbol) if start == -1 or end == -1: raise Exception("No symbol found") duration = end - start self.start = 0 self.end = duration # TODO storyboard? for idx, frame in enumerate(self.storyboard): self.storyboard[idx] = frame - start for t in self.tracks: t:ClipTrack for c in t.clips: c:Clip c.start = c.start - start c.end = c.end - start for cg in t.clip_groups: cg.retime() return self def trackClipGroupForFrame(self, track_idx, frame_idx, styles=[], check_end=True): for gidx, group in enumerate(self[track_idx].clip_groups): group:ClipGroup group.style_indices = styles if not check_end: end_good = False try: next_group = self[track_idx].clip_groups[gidx+1] end_good = next_group.start > frame_idx except IndexError: end_good = True if group.start <= frame_idx and ((check_end and group.end > frame_idx) or (not check_end and end_good)): if True: style_tracks = [self[sidx] for sidx in styles] if False: style_groups = None if styles: style_groups = [] for style in styles: style_groups.append(self.trackClipGroupForFrame(style, frame_idx, check_end=False)) return group.position(frame_idx, style_tracks) def clip_group(self, track_idx, f, styles=[]) -> ClipGroup: if track_idx > len(self.tracks)-1: return ClipGroup(None, -1, []) cg = self.trackClipGroupForFrame(track_idx, f.i if hasattr(f, "i") else f, styles) if cg: return cg else: return ClipGroup(self[track_idx], -1, []) clipGroup = clip_group def find_workarea(self, frame=None): if frame is None: frame = self.cti cg = self.trackClipGroupForFrame(self.workarea_track, frame) if cg: return [cg.start, cg.end] def jumps(self): js = self._jumps t = self[self.workarea_track] for clip in t.clips: js.insert(-1, clip.start) return js def text_for_frame(self, fi): cg = self.clip_group(self.workarea_track, fi) if cg and cg.currentSyllable(): cgs = cg.currentSyllable() if cgs.start == fi: return cgs.input_text ================================================ FILE: packages/coldtype-core/src/coldtype/timing/timeable.py ================================================ from dataclasses import dataclass from coldtype.interpolation import lerp, interp_dict from coldtype.runon.path import P from coldtype.timing.easing import ease, ez, applyRange from copy import copy import math class Timeable(): """ Abstract base class for anything with a concept of `start` and `end`/`duration` Implements additional methods to make it easier to work with time-based concepts """ def __init__(self, start, end, index=0, name=None, data={}, timeline=None, track=None, id=None, ): self.start = start self.end = end self.idx = index self.name = name self.feedback = 0 self.data = data self.timeline = timeline self.track = int(track) if track else 0 self.id = id self.timeables = [] @property def duration(self): return self.end - self.start def __bool__(self): if self.start == -1 and self.end == -1: return False else: return True def __repr__(self): if hasattr(self, "name") and self.name: return f"Timeable('{self.name}', {self.start}, {self.end} ({self.duration}))" else: return f"Timeable({self.start}, {self.end} ({self.duration}))" def delay(self, frames_delayed, feedback) -> 'Timeable': t = copy(self) t.start = t.start + frames_delayed t.end = t.end + frames_delayed t.feedback = feedback t.data = {} return t def retime(self, start=0, end=0, duration=-1): self.start = self.start + start self.end = self.end + end if duration > -1: self.end = self.start + duration return self def now(self, i): if self.start == self.end: return i == self.start else: return self.start <= i < self.end def _normalize_fi(self, fi): if hasattr(self, "timeline") and self.timeline: if self.end > self.timeline.duration and fi < self.start: return fi + self.timeline.duration return fi def _loop(self, t, times=1, cyclic=True, negative=False): lt = t*times*2 ltf = math.floor(lt) ltc = math.ceil(lt) if False: if ltc % 2 != 0: # looping back lt = 1 - (ltc - lt) else: # looping forward lt = ltc - lt lt = lt - ltf if cyclic and ltf%2 == 1: if negative: lt = -lt else: lt = 1 - lt return lt, ltf def at(self, i, every=None) -> "Easeable": if every: i = (i//every)*every return Easeable(self, i) @dataclass class EaseableTiming(): t: float = 0 i: int = -1 loopidx: int = 0 class Easeable(): def __init__(self, t:Timeable, i:int ): self.t:Timeable = t self.i:int = i self._ts = False if not isinstance(t, Timeable): self._ts = True @property def autowrap(self): return False def __repr__(self) -> str: return f"" def _normRange(self, rng, **kwargs): if "r" in kwargs: rng = kwargs["r"] if isinstance(rng, (int, float)): rng = (0, rng) return rng def _maxRange(self, rng, vs): if rng[1] > rng[0]: return max(vs) else: return min(vs) def every(self, i) -> "Easeable": self.i = (self.i//i)*i return self def inset(self, start=0, end=0) -> "Easeable": return Easeable(Timeable( start=self.t.start + start, end=self.t.end - end, name=self.t.name, data=self.t.data, track=self.t.track, timeline=self.t.timeline, ), self.i) @property def name(self): if self._ts: return "/".join([t.name for t in self.t]) else: return self.t.name def __bool__(self): if self._ts: return any([bool(t) for t in self.t]) else: return bool(self.t) @property def idx(self): if self._ts: if len(self.t) > 0: return self.t[0].idx else: return -1 else: return self.t.idx def index(self): if self._ts: return sum([self.i >= t.start for t in self.t]) - 1 else: return int(self.i >= self.t.start) - 1 def on(self, end=None): if self._ts: return bool(max([Easeable(t, self.i).on(end=end) for t in self.t])) if end is None: end = self.t.end if self.t.start == end: return self.i == self.t.start else: return self.t.start <= self.i < end def now(self): if not self._ts: if self.on(): return self else: return None for t in self.t: e = Easeable(t, self.i) if e.on(): return e return None def past(self): return self.on(end=10000000000) def tv(self, loops=0, cyclic=True, to1=True, choose=max, wrap=None, find=False, clip=True ): if wrap is None and self.autowrap: wrap = True if self._ts: es = [Easeable(t, self.i).tv(loops, cyclic, to1).t for t in self.t] if len(es) == 0: e = 0 else: e = choose(es) if find is False: return EaseableTiming(e) else: try: return EaseableTiming(e, es.index(e)) except: return EaseableTiming(e, -1) # chosen = [(i, 1 if x == e else 0) for i, x in enumerate(es)] # if e > 0: # return EaseableTiming(e, chosen[-1][0]) # else: # return EaseableTiming(e, chosen[0][0]) t, i = self.t, self.i if wrap: i = i % self.t.duration else: i = self.i if t.start == -1 and t.end == -1: return EaseableTiming(0) if clip and i < t.start: return EaseableTiming(0) elif clip and i > t.end: if loops % 2 == 0: return EaseableTiming(1) else: return EaseableTiming(0) else: d = t.duration if to1: d = d - 1 v = (i - t.start) / d if loops == 0: return EaseableTiming(max(0, min(1, v)) if clip else v) else: loop_t, loop_index = self.t._loop(v, times=loops, cyclic=cyclic, negative=False) return EaseableTiming(max(0, min(1, loop_t)) if clip else v, -1, loop_index) def e(self, easefn="eeio", loops=1, rng=(0, 1), on=None, # TODO? cyclic=True, to1=False, wrap=None, choose=max, find=False, loop_info=False, **kwargs ): rng = self._normRange(rng, **kwargs) if (not isinstance(easefn, str) and not isinstance(easefn, P) and not type(easefn).__name__ == "Glyph"): loops = easefn easefn = "eeio" tv = self.tv(loops, cyclic, to1, choose, wrap, find=True) ev = ez(tv.t, easefn, cyclic=cyclic, rng=rng) if find: return ev, tv.i elif loop_info: return ev, tv.loopidx else: return ev def ec(self, easefn="eeio", rng=(0, 1)): """ (e)asing-(c)umulative — for performing partial animations that accumulate over time, i.e. keys on a timeline `[0 ] [0 ]` will be added together, so the "final" value of those 0's (`.ki(0).ec("l")`) will be 2, not 1; use `rng` to define accumulation; max of range refers to max of each phase, not max of entire accumulation """ if self._ts: _ec = 0 for t in self.t: _ec += Easeable(t, self.i).e(easefn, 0, rng=rng) return _ec return self.e(easefn, 0, rng=rng) def interpDict(self, dicts, easefn, loops=0): v = self.tv(loops=loops).t vr = v*(len(dicts)-1) vf = math.floor(vr) v = vr-vf try: a, b = dicts[vf], dicts[vf+1] return interp_dict(ez(v, easefn), a, b) except IndexError: return dicts[vf] def io(self, length, easefn="eeio", negative=False, rng=(0, 1), **kwargs ): rng = self._normRange(rng, **kwargs) if self._ts: es = [Easeable(t, self.i).io(length, easefn, negative, rng) for t in self.t] return self._maxRange(rng, es) t = self.t try: length_i, length_o = length except: length_i = length length_o = length if isinstance(length_i, float): length_i = int(t.duration*(length_i/2)) if isinstance(length_o, float): length_o = int(t.duration*(length_o/2)) if self.i < t.start or self.i > t.end: return rng[0] try: ei, eo = easefn except ValueError: ei, eo = easefn, easefn to_end = t.end - self.i to_start = self.i - t.start easefn = None in_end = False if to_end < length_o and eo: in_end = True v = to_end/length_o easefn = eo elif to_start <= length_i and ei: v = to_start/length_i easefn = ei else: v = 1 if v == 1 or not easefn: return rng[1] else: return ez(v, easefn, 0, False, rng) #a, _ = ease(easefn, v) #return a if negative and in_end: return -a else: return a def adsr(self, adsr=(5, 0, 0, 10), es=("eei", "qeio", "eeo"), rng=(0, 1), dv=None, # decay-value rs=False, # read-sustain find=False, **kwargs ): rng = self._normRange(rng, **kwargs) if self._ts: es = [Easeable(t, self.i).adsr(adsr, es, rng, dv, rs) for t in self.t] mx = self._maxRange(rng, es) if find: return mx, es.index(mx) else: return mx if len(adsr) == 2: d, s = 0, 0 a, r = adsr elif len(adsr) == 3: d = 0 a, s, r = adsr elif len(adsr) == 4: a, d, s, r = adsr if rs: s = self.t.duration if len(es) == 2: de = "qeio" ae, re = es elif len(es) == 3: ae, de, re = es if dv is None: dv = rng[1] if d > 0: dv = lerp(rng[0], rng[1], 0.5) i, t = self.i, self.t end = t.start + d + s + r ds = t.start + d + s td = -1 if t.timeline: td = t.timeline.duration if i > end and td > -1: i = i - td rv = rng[0] if td > -1 and end > td: if i < t.start-a: i = i + td rv = Easeable(Timeable(ds, end), i+td).e(re, 0, rng=(dv, rng[0]), to1=1) if i < t.start: # ATTACK s = t.start - a out = self._maxRange(rng, [rv, Easeable(Timeable(t.start-a, t.start), i).e(ae, 0, rng=rng, to1=0)]) elif i >= t.start: if i == t.start: out = rng[1] if i >= ds: # RELEASE out = Easeable(Timeable(ds, end), i).e(re, 0, rng=(dv, rng[0]), to1=1) else: if i >= t.start + d: # SUSTAIN out = dv else: # DECAY out = Easeable(Timeable(t.start, ds), i).e(de, 0, rng=(rng[1], dv), to1=1) if find: return out, 0 else: return out ================================================ FILE: packages/coldtype-core/src/coldtype/timing/timeline.py ================================================ from collections import defaultdict from re import match from coldtype.timing.timeable import Timeable, Easeable from typing import List class Timeline(Timeable): """ General base class for any kind of timeline or sequence, in the NLE sense * `fps` is `frames-per-second`, and defaults to 30 * `storyboard` is useful if you want to always see a given frame when your animation loads for the first time, or you'd like to see multiple frames, i.e. `storyboard=[0, 5]` to see both frame 0 and frame 5 in the viewer * `tracks` is mostly used internally by subclasses that process external data from another NLE or data source like subtitles * `jumps` can be used to mark "jump-points" within a timeline, for easy skipping around with the up/down arrows (akin to up/down arrow movements in NLEs) """ __name__ = "Generic" def __init__(self, duration=None, fps=30, timeables=None, storyboard=None, jumps=None, start=None, end=None, findWords=True, name=None, ): self.timeables:List[Timeable] = self._flatten(timeables) self.name = name self.fps = fps if start is not None: self.start = start else: self.start = 0 if end is not None: self.end = end else: if duration is None: if len(self.timeables) > 0: #self.start = min([t.start for t in self.timeables]) self.end = max([t.end for t in self.timeables]) else: self.end = 0 else: self.end = duration self._holding = -1 self._jumps = [self.start, *(jumps or []), self.end-1] if not storyboard: self.storyboard = [0] else: self.storyboard = storyboard if len(self.storyboard) == 0: self.storyboard.append(0) self.storyboard.sort() if findWords: self.words = self.interpretWords(findWords) else: self.words = None def _flatten(self, timeables): if not timeables: return [] ts = [] for t in timeables: if isinstance(t, Timeable): ts.append(t) else: ts.extend(self._flatten(t)) return ts def jumps(self): return self._jumps def tracks(self): ts = defaultdict(lambda: []) for t in self.timeables: ts[t.track].append(t) return ts def text_for_frame(self, fi): return "" def __str__(self): return "".format(self.__name__, self.duration, self.fps, ",".join([str(i) for i in self.storyboard])) def __getitem__(self, index): return self.timeables[index] def __len__(self): return len(self.timeables) def __contains__(self, key): for t in self.timeables: if t.name == key: return True return False def hold(self, i): self._holding = i if self.words: self.words.hold(i) return self def at(self, i) -> Easeable: return Easeable(self.timeables, i) def timeable(self, name=None) -> Timeable: return Timeable( self.start, self.duration, name=name or "Clip", data=dict(), track=0, timeline=self) def easeable(self, i=None, name=None) -> Easeable: return Easeable(self.timeable(name), i or self._holding) def addClip(self, name=None, start=0, end=0): self.timeables.append(self.timeable(name).retime(start, -end)) return self def _keyed(self, k): k = str(k) all = [] if isinstance(k, str): for c in self.timeables: if k == "*" or c.name == k: all.append(c) if len(all) == 0: return Timeable(-1, -1) return all def k(self, *keys): if len(keys) > 1: es = [self.k(k) for k in keys] return self._flatten(es) else: return self._flatten(self._keyed(keys[0])) def _norm_held_fi(self, fi=None): if fi is None: if self._holding >= 0: return self._holding else: raise Exception("Must .hold or provide fi=") return fi % self.duration def ki(self, key, fi=None): """(k)eyed-at-(i)ndex""" fi = self._norm_held_fi(fi) if not isinstance(key, str): try: es = [self.ki(k, fi).t for k in key] return Easeable(self._flatten(es), fi) except TypeError: pass return Easeable(self._keyed(key), fi) def current(self, track=None, fi=None) -> Easeable: fi = self._norm_held_fi(fi) matches = [] for t in self.timeables: if t.now(fi): if track is not None and t.track != track: continue matches.append(t) return Easeable(matches, fi) def latest(self, track=None, fi=None) -> Easeable: fi = self._norm_held_fi(fi) matches = [] _fi = fi while _fi >= 0 and not matches: matches = self.current(track=track, fi=_fi).t or [] _fi -= 1 return Easeable(matches, fi) @property def tstart(self): if self._start > -1: return self._start _start = -1 for t in self.timeables: ts = t.start if _start == -1: _start = ts elif ts < _start: _start = ts return _start @property def tend(self): if self._end > -1: return self._end _end = -1 for t in self.timeables: te = t.end if _end == -1: _end = te elif te > _end: _end = te return _end def shift(self, prop, fn): for t in self.timeables: attr = getattr(t, prop) if callable(fn): setattr(t, prop, fn(t)) else: setattr(t, prop, attr + fn) return self def find_by_id(self, id): for t in self.timeables: if t.id == id: return t def findWordsWorkarea(self, fi): if self.words: cg = self.words.currentGroup(fi) if cg: return [int(cg.start), int(cg.end)] def interpretWords(self, include="*"): from coldtype.timing.sequence import ClipTrack, Clip includes = [] excludes = [] if isinstance(include, str): for x in include.split(" "): if x == "*": continue elif x.startswith("-"): excludes.append(int(x[1:])) elif x.startswith("+"): includes.append(int(x[1:])) clips = [] styles = [] for t in self.timeables: if len(includes) > 0: if t.track not in includes: continue if len(excludes) > 0: if t.track in excludes: continue if t.name.startswith("."): styles.append(Timeable(t.start, t.end, index=t.idx, name=t.name[1:], data=t.data, timeline=self, track=t.track)) else: start = t.start end = t.end if t.start == t.end: end = start + 1 if t.name == "•": start -= 1 end -= 1 clips.append(Clip(t.name, start, end, t.idx, track=t.track)) return ClipTrack(self, clips, None, Timeline(timeables=styles, findWords=False)) ================================================ FILE: packages/coldtype-core/src/coldtype/timing/viewer.py ================================================ from collections import defaultdict from coldtype.renderable.animation import animation, renderable from coldtype.text.composer import StSt, Font, Rect, Point, Style from coldtype.timing.timeline import Timeline from coldtype.timing.midi import MidiTimeline from coldtype.runon.path import P from coldtype.color import bw, hsl def timeViewer(tl): a = None ow = 1080 if isinstance(tl, animation): a = tl tl = a.t ow = a.rect.w lh = 40 lt = 40 lines = defaultdict(lambda: []) if False and isinstance(tl, MidiTimeline): for n in tl.notes: lines[n].append(n) elif isinstance(tl, Timeline): lines = tl.tracks() else: lines[0] = 1 try: line_count = max(lines.keys())+1 except ValueError: line_count = 0 def build_timeable_display(r): wu = r.w / int(tl.duration) rows = r.subdivide(line_count, "N") out = P() fs = 14 for line in lines.keys(): out += (P(rows[line].take(2, "N")) .f(bw(0.8)) .t(0, 1)) out += (StSt(str(line), Font.RecursiveMono(), fs) .align(rows[line].inset(-12, 0), "W") .f(hsl(0.65, 1, 0.7))) out += (StSt(str(line), Font.RecursiveMono(), fs) .align(rows[line].inset(-12, 0), "E") .f(hsl(0.65, 1, 0.7))) for t in tl.timeables: l = int(t.track) f = hsl(0.5+l*0.3) row = rows[l] tr = (row.take(t.duration*wu, "W") .offset(t.start*wu, 0)) out += (P(tr).f(f.lighter(0.2).with_alpha(0.8))) out += (P(row.take(2, "W")) .translate(t.start*wu, 0) .f(f)) out += (StSt(t.name, Font.RecursiveMono(), 34) .align(row.take(20, "W"), "W") .translate(t.start*wu+6, 0) .f(f.darker(0.15))) return out r = Rect(ow, line_count*lh+lt) re = r.inset(20, 0) rt, rd = re.divide(lt, "N") if isinstance(tl, Timeline): display = build_timeable_display(rd) else: display = P() outer = P([ #P(rw).f(bw(0.95)), P(rt).f(bw(0.95)) ]) frames = re.subdivide(tl.duration, "W") for idx, f in enumerate(frames): if idx%2==1: continue outer += (P(f).f(bw(0.9))) outer += display @renderable(r, xray=False, bg=1, preview_only=1) def timeViewBackground(r): return outer @animation(r , timeline=tl , bg=1 , preview_only=1 , sort=-1 , layer=1 , offset=a.offset , xray=False ) def timeView(f): x = f.e("l", 0, rng=(rd.psw[0], rd.pse[0])) return P([ P(Rect(2, r.h)).t(x, 0), (P().text(str(f.i), Style("Times", 20, load_font=0), Rect(x+6, r.h-20, 100, 40)))]) def pointToFrame(pt:Point): return round(min(1, max(0, (pt.x-re.x)/re.w))*tl.duration) timeView.pointToFrame = pointToFrame return timeViewBackground, timeView ================================================ FILE: packages/coldtype-core/src/coldtype/tool.py ================================================ from pathlib import Path from coldtype.geometry.rect import Rect from coldtype.text.font import Font def fmt_path(path: Path) -> str: try: return "~/" + str(path.relative_to(Path.home())) except ValueError: return str(path) def print_font_results(results, selected=None): maxsys = max([len(f.system_name) for f in results]) maxpat = max([len(fmt_path(f.path)) for f in results]) print("") print(f" # {'Name':<{maxsys}} Path") print(f" {'-'*(maxsys+maxpat+7)}") for idx, result in enumerate(results): if idx == selected: print(f"➡️ {idx:>{3}} {result.system_name:<{maxsys}} {fmt_path(result.path)}") else: print(f" {idx:>{3}} {result.system_name:<{maxsys}} {fmt_path(result.path)}") print("\n") def parse_inputs(inputs, defaults, ui=True, positional=True): if ui is not None and ui is not False: defaults["rect"] = [ Rect(1080, 1080), lambda xs: Rect([int(x) for x in str(xs).split(",")])] defaults["preview_only"] = [False, bool] defaults["log"] = [False, bool] parsed = {} if not isinstance(inputs, dict): for idx, input in enumerate(inputs): if "=" in input: k, v = input.split("=") parsed[k] = v elif input in defaults.keys(): parsed[input] = True elif positional: try: parsed[list(defaults.keys())[idx]] = input except KeyError: pass else: parsed = {**inputs} out = {} font_variations = {} out["font_variations"] = {} for k, v in defaults.items(): if k in ["w", "h"]: out[k] = v defaults[k] = [v, int] if k == "font" and v[0] is not None: out[k] = v[0] out["fonts"] = [v][0] out["font_variations"] = v[0].variations() out["fontSize"] = 42 else: out[k] = v[0] if k not in parsed and len(v) > 2: raise Exception(v[2]) for k, v in parsed.items(): if k in defaults: if defaults[k][0] is None and v is None: pass else: if isinstance(v, str): if k == "font": sized = v.split(":") if len(sized) > 1: out["fontSize"] = int(sized[1]) else: out["fontSize"] = 72 v = sized[0] vs = v.split("@") fnt_idx = 0 if len(vs) > 1: fnt_idx = int(vs[1]) fonts = Font.ListAll(vs[0]) if len(fonts) == 0: print(f"\n\n‼️ Search \"{v}\" returned no fonts ‼️\n") out[k] = Font.ColdtypeObviously() else: out["fonts"] = fonts print_font_results(fonts, fnt_idx) out[k] = fonts[fnt_idx] font_variations = out[k].variations() elif k == "rect": if v == "max": out[k] = ui.get("monitor").scale(2).inset(200).square().zero() else: out[k] = eval(f"Rect({v})") #raise Exception("IMPLEMENT MAX with screen size") elif defaults[k][1] == bool: out[k] = bool(eval(v)) else: out[k] = defaults[k][1](v) else: if k == "rect": print("ALSO HERE") out[k] = Rect(v) else: out[k] = v else: if k in font_variations: out["font_variations"][k] = float(v) else: print(f"> key {k} not recognized") return out ================================================ FILE: packages/coldtype-core/src/coldtype/tools/chars.py ================================================ """ Display characters available in a font """ from coldtype import * from coldtype.tool import parse_inputs, fmt_path from coldtype.osutil import show_in_finder args = parse_inputs(ººinputsºº, dict( font=[None, str, "Must provide font regex or path"], rect=[Rect(1080), int]), ui=ººuiºº) print("👉 Click something to see information printed in the terminal\n") @animation(Rect(args["rect"].w, args["rect"].h+(h:=120)), bg=1, tl=Timeline(len(args["fonts"]))) def chars_display(f): fnt = args["fonts"][f.i] path = fmt_path(fnt.path) chars = fnt.chars() sq = math.ceil(math.sqrt(len(chars))) header, grid = f.a.r.divide(h, "N") rs = grid.inset(10).grid(sq, sq) return P( P(header).f(0), StSt(fnt.names()[0], Font.JBMono(), 40, wght=1).align(header.inset(30), "N").f(1), StSt(path, Font.JBMono(), 20, wght=0.25).align(header.inset(25), "S").f(0.75), P().gridlines(grid, sq, sq), P().enumerate(chars, lambda x: StSt(x.el[0], fnt, rs[0].h-10, variations=args["font_variations"]) .data(char=x.el[0], gn=x.el[1]) .f(0) .align(rs[x.i]))).data(fontPath=fnt.path) def build(_): show_in_finder(chars_display.last_return.data("fontPath")) def on_click(pos): for m in (chars_display.last_return .find(lambda x: x.data("char") and pos.inside(x.ambit()))): char = m.data("char") print(f"> {char} \\u{ord(char):04X} {m.data('gn')}") ================================================ FILE: packages/coldtype-core/src/coldtype/tools/dsview.py ================================================ """ Display available designspace """ from coldtype import * from coldtype.tool import parse_inputs, fmt_path from coldtype.osutil import show_in_finder args = parse_inputs(ººinputsºº, dict( font=[Font.MutatorSans(), str], text=["A", str], count=[9, int], axes=["0,1", str], ) , ui=ººuiºº) A, B = [int(x) for x in args["axes"].split(",")] a, b = A, B @animation(Rect(args["rect"].w, args["rect"].h+(h:=120)), bg=1, tl=Timeline(len(args["fonts"]))) def dsview_display(f): global a, b fnt:Font = args["fonts"][f.i] path = fmt_path(fnt.path) variations = fnt.variations() axes = list(variations.keys()) variations = list(variations.items()) if len(axes) == 1: a, b = 0, 0 else: a, b = A, B l = Scaffold(f.a.r).cssgrid("85 a", f"{h} a 85", "h h / a2 g / x a1") def draw_glyph(x, y): r = y.el.inset(4) return (P( StSt(args["text"], fnt, args["fontSize"], variations={axes[a]:x.e, axes[b]:y.e}) .align(r) .f(0) .data(font=fnt, position=dict(x=x.e, y=y.e)))) return (P( P(l["h"].r).f(1), StSt(fnt.names()[0], Font.JBMono(), 40, wght=1).align(l["h"].r.inset(25), "NE").f(0), StSt(path, Font.JBMono(), 22, wght=0.25).align(l["h"].r.inset(25), "SE").f(0), horizontal:=StSt(fnt.getName(variations[a][1].get("axisNameID")) + f" : “{variations[a][0]}” ", Font.JBMono(), 24, wght=1) .align(l["a1"].r, "W") .f(0) .t(d:=100, 0), vertical:=StSt(fnt.getName(variations[b][1].get("axisNameID")) + f" : “{variations[b][0]}” ", Font.JBMono(), 24, wght=1) .rotate(90) .unframe() .align(l["a2"], "S") .t(0, d) .f(0), P().enumerate((g:=l["g"].r).subdivide(args["count"], "W"), lambda x: P().enumerate(x.el.subdivide(args["count"], "S"), lambda y: draw_glyph(x, y))), P().line(l["h"].r.es).ep().fssw(-1, 0.75, 1), P().line(l["a2+x"].r.ee).ep().fssw(-1, 0.75, 1), P().line(l["a1+x"].r.en).ep().fssw(-1, 0.75, 1), P().gridlines(g, args["count"]).fssw(-1, 0.75, 1), StSt(str(int(variations[a][1].get("minValue"))), Font.JBMono(), 20, wght=1).align(l["a1"].r, "W").t(d:=20, 0).f(fill:=bw(0.65)), StSt(str(int(variations[a][1].get("maxValue"))), Font.JBMono(), 20, wght=1).align(l["a1"].r, "E").t(-d, 0).f(fill), StSt(str(int(variations[b][1].get("minValue"))), Font.JBMono(), 20, wght=1).align(l["a2"].r, "S").t(0, d).f(fill), StSt(str(int(variations[b][1].get("maxValue"))), Font.JBMono(), 20, wght=1).align(l["a2"].r, "N").t(0, -d).f(fill), P().line([p:=horizontal.ambit().pe.project(0, 30), p2:=p.project(0, ln:=350)]).ep() .line([p2, p2.project(45*3, ad:=14)]).ep() .line([p2, p2.project(-45*3, ad)]).ep() .fssw(-1, 0, 2), P().line([p:=vertical.ambit().pn.project(90, 30), p2:=p.project(90, ln)]).ep() .line([p2, p2.project(45*-1, ad)]).ep() .line([p2, p2.project(45*5, ad)]).ep() .fssw(-1, 0, 2), ).data(fontPath=fnt.path)) def build(_): show_in_finder(dsview_display.last_return[1].data("fontPath")) def on_click(pos): for m in (dsview_display.last_return.find(lambda x: x.data("position") and pos.inside(x.ambit()))): variations = list([v[1] for v in m.data("font").variations().items()]) range_0 = variations[a].get("maxValue") - (min:=variations[a].get("minValue")) pos_0 = int(m.data("position").get("x")*range_0 + min) range_1 = variations[b].get("maxValue") - (min:=variations[b].get("minValue")) pos_1 = int(m.data("position").get("y")*range_1 + min) print(f"Position: {variations[a].get("axisTag")} {pos_0}, {variations[b].get("axisTag")} {pos_1} / {variations[a].get("axisTag")} {m.data("position").get("x")}, {variations[b].get("axisTag")} {m.data("position").get("y")}") ================================================ FILE: packages/coldtype-core/src/coldtype/tools/find.py ================================================ """ Find all fonts on computer matching passed regex - args: [font(search), dst] - build: duplicates fonts to dst """ from coldtype import * from coldtype.tool import parse_inputs from subprocess import run args = parse_inputs(__inputs__, dict( font=[None, str, "Must provide search string"], dst=["~/Desktop", str])) results = args["fonts"] results = results[:30] if len(results) > 0: def build_preview(x): return (P( StSt(str(x.i), Font.JBMono(), 30, wght=1) .t(0, 8), StSt(x.el.names()[0], x.el, args["fontSize"]) .t(60, 0), P().rect(Rect(50, 2))) .data(font=x.el)) previews = P().enumerate(results, build_preview) w = max([p.ambit().w for p in previews]) h = sum([p.ambit().h for p in previews]) rect = Rect(w+20, h + 20*(len(results)+1)) @renderable(rect, bg=0) def show_results(r): return (previews .copy() .stack(20) #.xalign(r) .align(r) .f(1)) def build(_): for font in results: print(f" > Duplicated: {font.copy_to(args['dst'], return_dst=True)}") numpad = { 1: lambda _: run(["open", "-a", "FontGoggles", *[f.path for f in results]]) } else: @renderable(540, bg=0) def no_results(r): return (StSt("NO\nRESULTS", Font.JBMono(), 72, wdth=1, wght=1) .align(r) .f("hotpink")) #def build(_): # from coldtype.osutil import show_in_finder # show_in_finder(fnt.path) ================================================ FILE: packages/coldtype-core/src/coldtype/tools/findappicon.py ================================================ import subprocess, plistlib, shutil from coldtype import * from coldtype.raster import * from coldtype.tool import * # mac-only args = parse_inputs(ººinputsºº, dict( app=[None, str, "Must provide app name"], size=[512, str], )) size = args["size"] app_name = args["app"] if size == "@1x": size = 512 elif size == "@2x": size = 1024 elif size == "@3x": size = 1536 elif size == "@4x": size = 2048 else: size = int(size) result = subprocess.run( ['mdfind', f'kMDItemKind == "Application" && kMDItemDisplayName == "*{app_name}*"'], capture_output=True, text=True) tmp = Path("/tmp") paths = [Path(line) for line in result.stdout.strip().split('\n') if line.endswith('.app')] apps = [] for path in paths: plist = path / "Contents" / "Info.plist" if plist.exists(): with open(plist, "rb") as f: plist = plistlib.load(f) icon = plist.get("CFBundleIconFile") or plist.get("CFBundleIconName") icon = path / "Contents/Resources" / icon if icon.exists(): tmp_output = tmp / f"_coldtype_findappicon_{path.stem}_{size}.png" subprocess.run(['sips', '-s', 'format', 'png', '-z', str(size), str(size), str(icon), '--out', tmp_output], check=True, capture_output=True) apps.append([path, tmp_output]) @animation(Rect(size, size), tl=Timeline(len(apps))) def icon_viewer(f:Frame): return SkiaImage(apps[f.i][1]) def release(_): for _, img in apps: shutil.copy(img, img.name) ================================================ FILE: packages/coldtype-core/src/coldtype/tools/glyphloop.py ================================================ from coldtype import * from coldtype.tool import parse_inputs args = parse_inputs(__inputs__, dict( font=[None, str, "Must provide font regex or path"], showChars=[False, bool])) fnt = args["font"] os2 = fnt.font.ttFont["OS/2"] glyphSet = fnt.font.ttFont.getGlyphSet() glyphs = list(glyphSet.keys()) @animation((1920, 1080), tl=Timeline(len(glyphs), 24), bg=0) def glyphViewer(f): glyphKey = glyphs[f.i] glyph = glyphSet[glyphKey] return (P( P().glyph(glyph, glyphSet).data(frame=Rect(0, 0, glyph.width, os2.sCapHeight)) .f(1) .align(f.a.r, tx=0) .scale(0.5, tx=0) .scaleToRect(f.a.r.inset(100), shrink_only=True, preserveAspect=True), StSt("{:04d}".format(f.i), fnt, 60) .f(1) .align(f.a.r.inset(100), "SW", tx=0) .null())) ================================================ FILE: packages/coldtype-core/src/coldtype/tools/glyphs.py ================================================ from coldtype import * from coldtype.tool import parse_inputs # TODO single-frame animated version? args = parse_inputs(__inputs__, dict( font=[None, str, "Must provide font regex or path"], fontSize=[100, int])) fnt = args["font"] os2 = fnt.font.ttFont["OS/2"] glyphSet = fnt.font.ttFont.getGlyphSet() els = glyphSet.keys() fs = args["fontSize"] glyphs = P().enumerate(els, lambda x: P() .glyph(glyphSet[x.el], glyphSet) .f(0) .scale(fs/fnt.metrics.upem, pt=(0,0))) maxr:Rect = max([g.ambit() for g in glyphs]) maxr = maxr.square(outside=True).round().inset(-10).zero() sq = math.ceil(math.sqrt(len(els))) rect = Rect(sq*maxr.w, sq*maxr.w) @ui(rect, bg=1) def wt1(u): rs = u.r.inset(10).grid(sq, sq) def showChar(x): if u.c.inside(rs[x.i]): print(">", x.el) return glyphs[x.i].copy().align(rs[x.i]) return P( P().gridlines(u.r.inset(10), sq, sq), P().enumerate(els, showChar)) def build(_): from coldtype.osutil import show_in_finder show_in_finder(fnt.path) ================================================ FILE: packages/coldtype-core/src/coldtype/tools/instances.py ================================================ """ Display all available instances in variable font; click instance to see more info """ from coldtype import * from coldtype.tool import parse_inputs from pprint import pprint if __as_config__: raise ColdtypeCeaseConfigException() args = parse_inputs(ººinputsºº, dict( font=[None, str, "Must provide font"])) font = args["font"] instances = font.instances() unscaled = font.instances(scaled=False) if len(instances) > 0: print(f"=== {font.path.name} ===") print(f"=== {len(instances)} instances ===") print("") vectors = (P().enumerate(instances.keys(), lambda x: StSt(x.el, font, 80, instance=x.el) .data( instanceKey=x.el, instance=instances[x.el], unscaled=unscaled[x.el]) ).stack(10)) a = vectors.ambit() h = max(a.h, 1600) r = Rect(a.w*h/a.h, h) @ui(r.inset(-20).zero(), bg=0) def display(u): def print_instance(x): un = x.data("unscaled") divider = "-"*(len(str(un)) + len("Unscaled: ") + 3) print(divider) print(f" Instance: “{x.data('instanceKey')}”") print(" Scaled: ", x.data("instance")) print(" Unscaled: ", un) print(divider) print("") return x return (vectors .copy() .scaleToRect(r) .align(u.r) .f(1) .map(lambda p: p.cond(u.c.inside(p.ambit()), print_instance))) ================================================ FILE: packages/coldtype-core/src/coldtype/tools/midi.py ================================================ from coldtype import * from coldtype.timing.midi import MidiTimeline from coldtype.tool import parse_inputs if __as_config__: raise ColdtypeCeaseConfigException() args = parse_inputs(__inputs__, dict( file=[None, str, "Must provide midi file"], duration=[None, int], bpm=[None, float], fps=[None, float], text=[True, bool], lookup=[None, {}])) mr = MidiTimeline( Path(args["file"]).expanduser(), duration=args["duration"], fps=args["fps"], bpm=args["bpm"], lookup=args["lookup"]) dst = Path(args["file"]).parent custom_folder = Path(args["file"]).name + ".midiview/renders" if args["log"]: print("="*20) print("> Path:", mr.midi_path) print(f"> Note Range: {mr.min}-{mr.max}") print("> Duration:", mr.duration) print(f"> BPM/FPS: {mr.bpm}/{mr.fps}") print("="*20) r = args["rect"] xo = 47 def build_display(): rt, rd = r.divide(30, "N") wu = (rd.w - xo) / int(mr.duration) valid_notes = set() for t in mr.timeables: valid_notes.add(int(t.name)) valid_notes = sorted(valid_notes) rows = rd.subdivide(len(valid_notes), "S") out = P( P(rt).f(hsl(0.6, 1, 0.85)), P().line(rd.subtract(xo, "W").ew) .fssw(-1, hsl(0.65, 1, 0.8), 1)) for idx, vn in enumerate(valid_notes): out += P(rows[idx]).f(1 if idx%2==0 else hsl(0.6, 1, 0.975)) out += P().line(rows[idx].es).fssw(-1, hsl(0.65, 1, 0.8), 1) if args["text"]: out += StSt(f"{vn}", Font.RecursiveMono(), 20).align(rows[idx].take(xo, "W")).f(hsl(0.65)) else: out += P().text(f"{vn}", Style("Monaco", 24, load_font=0), rows[idx].take(xo, "W").offset(8, rows[idx].h/2-21/2)) for t in mr.timeables: t:Timeable i = valid_notes.index(int(t.name)) tr = (rows[i].take(t.duration*wu, "W")) out += (P(tr) .translate(xo+t.start*wu, 0) .f(hsl(0.5+t.idx*0.02))) return rt, rd, out rt, rd, static = build_display() @animation(r, timeline=mr, dst=dst, custom_folder=custom_folder, bg=1, render_bg=1, preview_only=args["preview_only"]) def midi(f): px = f.e("l", 0, rng=(xo, rd.w)) if args["text"]: frame = (StSt(str(f.i), Font.RecursiveMono(), 20) .align(rt.take(px-10, "W"), "E") .f(hsl(0.65))) else: frame = P().text("{:04d}".format(f.i), Style("Monaco", 18, load_font=0, fill=0), rt.offset(px-50, 8)) return P( static, frame, P().line(r.ew) .translate(xo, 0) .fssw(-1, bw(0, 0.25), 1), P().line(r.ew) .translate(px, 0) .fssw(-1, 0, 1)) ================================================ FILE: packages/coldtype-core/src/coldtype/tools/midicc.py ================================================ from coldtype import * from coldtype.tool import parse_inputs if __as_config__: raise ColdtypeCeaseConfigException() args = parse_inputs(__inputs__, dict( port=[None, str, "Must provide port name"], channel=[1, int])) r = args["rect"] @animation(r, bg=0, rstate=1, preview_only=args["preview_only"]) def midicc(f, rs): if 0: print(rs.midi) controls = [] for k, v in rs.midi.items(): for c, data in v.items(): for note, value in data.items(): if not note.startswith("_"): controls.append([k, c, note, value]) rs = f.a.r.subdivide(len(controls), "W") def show_cc(x): _r = rs[x.i].inset(1) k, c, note, value = x.el return P( P(_r).f(hsl(0.9, 0.7, 0.8)), StSt("A", Font.MuSan(), 100, wdth=value/127, wght=value/127).align(_r), P( StSt(str(value), Font.RecMono(), 42), StSt(note, Font.RecMono(), 52), StSt(f"{k} / 1", Font.RecMono(), 24), ) #.map(lambda p: p.scaleToWidth(_r.w, shrink_only=1)) .xalign(_r, "W", tx=0) .align(_r) .stack(10) .align(_r.inset(0), "SW", tx=0, ty=0) ) return P().enumerate(controls, show_cc) ================================================ FILE: packages/coldtype-core/src/coldtype/tools/vf.py ================================================ from coldtype import * from coldtype.tool import parse_inputs from pprint import pprint from itertools import combinations from random import Random #if __as_config__: # raise ColdtypeCeaseConfigException() args = parse_inputs(ººinputsºº, dict( font=[None, str, "Must provide font"], font_size=[None, int], positions=[(0, 1), lambda xs: [float(x) for x in xs.split(",")]], stroke=[False, bool], text=["A", str], seed=[0, int], shuffle=[False, bool], animate=[True, bool])) rnd = Random() rnd.seed(args["seed"]) if isinstance(args["font"], str): args["font"] = Font.Find(args["font"]) dst = args["font"].path custom_folder = dst.name + ".vfview/renders" axes = args["font"].variations() if args["log"]: pprint(axes) possibles = [] for a in axes.keys(): for x in args["positions"]: possibles.append([a, x]) valids = [] for c in combinations(possibles, len(axes.keys())): v = True for idx, a in enumerate(c): for jdx, a2 in enumerate(c): if idx != jdx and a[0] == a2[0]: v = False if v: valids.append(c) axes_combos = [] for combo in valids: axs = {} for k, v in combo: axs[k] = v axes_combos.append(axs) sq = math.floor(math.sqrt(len(axes_combos))) r:Rect = args["rect"] rs = r.inset(20).grid(sq, math.ceil(len(axes_combos)/sq)) if args["shuffle"]: rnd.shuffle(axes_combos) @animation(r, timeline=60 if args["animate"] else 1, dst=dst, custom_folder=custom_folder, bg=0, render_bg=1, preview_only=args["preview_only"]) def vf(f): if args["animate"]: anim_combos = [] for ac in axes_combos: anim_combo = {} for k, v in ac.items(): anim_combo[k] = f.e("eeio", 1, rng=(v, v+1)) anim_combos.append(anim_combo) else: anim_combos = axes_combos def txt(x): #return P(rs[x.i].inset(20)) return P( StSt(args["text"], args["font"], args["font_size"] or rs[x.i].h-50, rv=1, **x.el) .align(rs[x.i], tx=0) .cond(args["stroke"], lambda p: p.fssw(-1, 1, 2), lambda p: p.f(1)), P().text(",".join(["{:0.2f}".format(v) for v in x.el.values()]), Style(Font.RecursiveMono(), 24, fill=bw(1, 0.5), load_font=0), rs[x.i].inset(50, 0))) return (P().enumerate(anim_combos, txt)) ================================================ FILE: packages/coldtype-core/src/coldtype/tools/viewseq.py ================================================ from coldtype import * from coldtype.tool import parse_inputs from coldtype.renderable.animation import image_sequence from coldtype.img.skiaimage import SkiaImage args = parse_inputs(__inputs__, dict( path=[None, str, "Must provide path to image sequence"], fps=[30, float], fmt=["h264", str], date=[False, bool], loops=[1, int], reverse=[False, bool], audio=[None, str], set709=[True, bool], dirsort=["x.name", str], )) dirsort = eval(f"lambda x: {args['dirsort']}") def find_pngs(_root): return sorted(list(_root.glob("*.png")), key=lambda p: p.stem.split("_")[-1]) root = Path(args["path"]).expanduser().absolute() if not root.exists(): raise Exception("viewseq root path not found") images = find_pngs(root) if len(images) == 0: for dir in sorted(filter(lambda x: not x.name.startswith("."), root.iterdir()), key=dirsort): if not dir.name.startswith("."): images.extend(find_pngs(dir)) def releaser(x:animation): fe = FFMPEGExport(x, date=args["date"], loops=args["loops"], audio=args["audio"], set_709=args["set709"], output_folder=root.parent) if args["fmt"] == "h264": fe.h264() elif args["fmt"] == "mov": fe.prores() elif args["fmt"] == "gif": fe.gif() fe.write(verbose=True, name=root.stem) fe.open() @image_sequence(images, args["fps"], looping=args["reverse"], release=releaser) def viewseq(_): return None # def release(): # dst_dir = images[0].parent.parent.parent # dst_name = images[0].parent.parent.stem # print(dst_dir, dst_name) ================================================ FILE: packages/coldtype-core/src/coldtype/warping.py ================================================ # legacy alias / deprecated from coldtype.fx.warping import * ================================================ FILE: packages/coldtype-core/src/coldtype/web/fonts.py ================================================ from coldtype.text.font import Font from pathlib import Path from dataclasses import dataclass from typing import List from shutil import copy2 @dataclass class WebFont(): font: Font woff2: Path woff2_relative: Path bold: bool italic: bool variations: dict embedded: str = None def js_args(self): return ", ".join([f"{k}={(v['defaultValue']-v['minValue'])/(v['maxValue']-v['minValue'])}" for k, v in self.variations.items()]) def js_setter(self): return ", ".join([f"'{k}' ${{{v['minValue']}+({v['maxValue']}-{v['minValue']})*{k}}}" for k, v in self.variations.items()]) @dataclass class WebFontFamily(): name: str variable_name: str fonts: List[WebFont] embed: bool = False @property def variable_name_js(self): return self.variable_name.replace("-", "_") def get_woff2(root, dst_folder, font_file, bold=False, italic=False, features={}): #src = Font.Cacheable(src_folder / font_file) src = Font.Find(font_file) if Path(src.path).suffix == ".woff2": woff2_src = Path(src.path) else: woff2_src = Path(src.path).with_suffix(".woff2") woff2_dst:Path = dst_folder / woff2_src.name has_been_modded = False if woff2_dst.exists(): src_mtime = Path(src.path).stat().st_mtime dst_mtime = woff2_dst.stat().st_mtime has_been_modded = dst_mtime < src_mtime if not woff2_dst.exists() or has_been_modded: if woff2_src.exists(): copy2(woff2_src, woff2_dst) else: src.subset(woff2_dst, features=features) variations = src.variations() for _, vs in variations.items(): try: del vs["axisNameID"] del vs["flags"] except KeyError: pass vs["spread"] = vs["maxValue"] - vs["minValue"] return WebFont(src, woff2_dst, woff2_dst.relative_to(root), bold, italic, variations) def woff2s(dst_folder, families_info, root): dst_folder = Path(dst_folder).expanduser() dst_folder.mkdir(exist_ok=True, parents=True) families = [] for var_name, family in families_info.items(): fonts = [] features = family.get("_features", {}) embed = "_embed" in family for style_name, font in family.items(): if not style_name.startswith("_"): fonts.append(get_woff2(root, dst_folder, font, bold="bold" in style_name, italic="italic" in style_name, features=features)) _, family = fonts[-1].font.names() families.append(WebFontFamily(family.replace(" ", ""), var_name, fonts, embed=embed)) return families if __name__ == "__main__": woff2s("assets/fonts", { "display-font": dict( regular="OhnoCasualVariable.woff2"), "text-font": dict( regular="DegularVariable.woff2")}) ================================================ FILE: packages/coldtype-core/src/coldtype/web/page.py ================================================ import re, frontmatter, markdown, json from pathlib import Path from dataclasses import dataclass from datetime import datetime from bs4 import BeautifulSoup from jinja2 import Template from pygments import highlight from pygments.lexers import PythonLexer from pygments.formatters import HtmlFormatter from lxml.html import fragment_fromstring, tostring from coldtype.img.skiaimage import SkiaImage def count_words_in_markdown(markdown): text = markdown text = re.sub(r'', '', text, flags=re.MULTILINE) text = text.replace('\t', ' ') text = re.sub(r'[ ]{2,}', ' ', text) text = re.sub(r'^\[[^]]*\][^(].*', '', text, flags=re.MULTILINE) text = re.sub(r'^( {4,}[^-*]).*', '', text, flags=re.MULTILINE) text = re.sub(r'{#.*}', '', text) text = text.replace('\n', ' ') text = re.sub(r'!\[[^\]]*\]\([^)]*\)', '', text) text = re.sub(r']*>', '', text) text = re.sub(r'[#*`~\-–^=<>+|/:]', '', text) text = re.sub(r'\[[0-9]*\]', '', text) text = re.sub(r'[0-9#]*\.', '', text) return len(text.split()) def wrap_images_with_links(html_string, grab_image, root:Path): soup = BeautifulSoup(html_string, 'html.parser') img_tags = soup.find_all('img') for img_tag in img_tags: a_tag = soup.new_tag('a') src = img_tag["src"] src_path = root / Path(src[1:]) if not src_path.exists(): print("Image not found", src_path) img_tag['style'] = f"width:200px;height:200px" else: img = SkiaImage(src_path) width = img.width() mtime = int(src_path.stat().st_mtime) tq = "?t=" + str(mtime) img_tag['src'] = src + tq hires_version = src_path.parent / "hires" / src_path.name hires_link = None if hires_version.exists(): hires_link = "/" + str(hires_version.relative_to(root)) + tq a_tag['href'] = img_tag['src'] a_tag["target"] = "_blank" a_tag['style'] = f"max-width:{int(width/2)}px" img_tag['style'] = f"max-width:{int(width/2)}px" figcaption = img_tag.find_parent('figure').find("figcaption") img_tag['alt'] = figcaption.get_text(strip=True) img_tag.wrap(a_tag) if hires_link: existing_text = figcaption.string new_link = soup.new_tag('a', href=hires_link) new_link.string = "Hi-Res" figcaption.clear() # Clear existing content figcaption.append(existing_text or "") figcaption.append(' / ') figcaption.append(new_link) for p_tag in soup.find_all('p'): length = len(p_tag.get_text(strip=True)) if length == 0: p_tag.extract() if grab_image is not None: try: first_figure = soup.find("figure") first_figure.extract() first_figure.find("a")["href"] = grab_image first_figure.find("a")["target"] = "" first_figure.find("figcaption").extract() return str(first_figure), str(soup) except AttributeError: pass return None, str(soup) def md_process(text, grab_image, root:Path): #text = text.replace("?_", "?_ ") raw = markdown.markdown(text, extensions=["smarty", "markdown_captions", "footnotes"]) #return None, raw return wrap_images_with_links(raw, grab_image, root) @dataclass class Page: path: Path date: str slug: str title: str template: str preview: str content: str data: dict preview_image: str def date_rfc_822(self): if "/" in str(self.date): input_date = datetime.strptime(str(self.date) + " 18:00:00 +0000", "%m/%d/%Y %H:%M:%S %z") else: input_date = datetime.strptime(str(self.date) + " 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z") return input_date.strftime("%a, %d %b %Y %H:%M:%S %z") def word_count(self): return str(count_words_in_markdown(self.data.content)) def image_count(self): count = 0 for line in self.data.content.split("\n"): if line.strip().startswith('!['): count += 1 return str(count) def unpublished(self): return self.date is None def output_path(self, sitedir:Path): op = sitedir / self.slug if str(op).endswith(".html"): return op else: return op / "index.html" @staticmethod def get_slug(file:Path, root:Path, slugs:str): if "nested" in slugs: slug = str(Path(file).relative_to(root / "pages").with_suffix("")) else: slug = file.stem if "html" in slugs: return slug + ".html" return slug @staticmethod def load_notebook(file:Path, root:Path, template:Template, template_fn=None, slugs="flat") -> "Page": data = json.loads(file.read_text()) frontmatter = eval("".join(data["cells"][0]["source"])) default_slug = Page.get_slug(file, root, slugs) slug = frontmatter.get("slug", default_slug) frontmatter["notebook"] = True cells = [] for c in data["cells"][1:]: ct = c["cell_type"] if ct == "markdown": cells.append(dict(text=markdown.markdown("".join(c["source"]), extensions=["smarty", "fenced_code"]))) elif ct == "code": src = "".join(c["source"]) if src.strip().startswith("#hide-publish") or src.strip().startswith("#hide-blog"): continue lines = [] for line in src.splitlines(): if not line.strip().endswith("#hide-publish") and not line.strip().endswith("#hide-blog"): lines.append(line) src = "\n".join(lines) highlit = fragment_fromstring( highlight(src, PythonLexer(), HtmlFormatter(linenos=False))) html = tostring(highlit, pretty_print=True, encoding="utf-8").decode("utf-8") cell = dict(html=html) outputs = [] if "outputs" in c: for o in c["outputs"]: if o["output_type"] == "display_data" and o["data"] and "text/html" in o["data"]: outputs.append(o["data"]["text/html"]) #print(o["data"]["text/html"][:10]) elif o.get("name") == "stdout": outputs.append(["
" + "".join(o["text"]) + "
"]) #print(outputs[-1][10]) else: pass if len(outputs) > 0: cell["outputs"] = outputs else: cell["no_outputs"] = True cells.append(cell) notebook_html = template.render(cells=cells) title = frontmatter.get("title", "Untitled") if template_fn: page_template = template_fn(data) else: page_template = data.get("template") if page_template is None: page_template = "_" + str(file.parent.stem)[:-1] return Page(file, frontmatter.get("date"), slug, title, page_template, None, notebook_html, frontmatter, None) @staticmethod def load_markdown(file:Path, root:Path, template_fn=None, slugs="flat") -> "Page": data = frontmatter.loads(file.read_text()) default_slug = Page.get_slug(file, root, slugs) slug = data.get("slug", default_slug) title = data.get("title", "Untitled") if template_fn: template = template_fn(data) else: template = data.get("template") if template is None: template = "_" + str(file.parent.stem)[:-1] preview_txt = data.content.split("+++")[0].replace("ßßß", "") preview_txt = re.sub(r'\[\^(\d+)\]', '', preview_txt) preview_image, preview = md_process(preview_txt, f"/{slug}", root) _, content = md_process(data.content .replace("+++", "
•••
") .replace("ßßß", "
") .replace("---", "
•••
"), None, root) return Page(file, data.get("date"), slug, title, template, preview, content, data, preview_image) ================================================ FILE: packages/coldtype-core/src/coldtype/web/server.py ================================================ import socket, subprocess, os, signal from pathlib import Path try: import livereload except ImportError: print("> pip install livereload") def is_port_in_use(port: int) -> bool: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: return s.connect_ex(('localhost', port)) == 0 def maybe_run_server(livereload:bool, port:int, dir:Path): if not is_port_in_use(port): if livereload: os.system(" ".join(["livereload", "-p", str(port), str(dir), "&>/dev/null", "&"])) else: os.system(f"python -m http.server {port} -d {dir} &>/dev/null &") return True else: return False def kill_process_on_port_unix(port): os.system(f"lsof -i tcp:{port} | awk 'NR!=1 {{print $2}}' | xargs kill") ================================================ FILE: packages/coldtype-core/src/coldtype/web/site.py ================================================ import jinja2, os, re, subprocess, shutil from coldtype.renderable import renderable from coldtype.geometry import Rect from coldtype.text import Style from coldtype.web.server import maybe_run_server, kill_process_on_port_unix from coldtype.web.fonts import woff2s from coldtype.web.page import Page from pathlib import Path from random import randint from datetime import datetime, timezone generics_folder = Path(__file__).parent / "templates" generics_env = jinja2.Environment(loader=jinja2.FileSystemLoader(generics_folder)) string_env = jinja2.Environment(loader=jinja2.BaseLoader()) try: from sourcetypes import jinja_html, css, js except ImportError: jinja_html = str nav_template: jinja_html = """ """ class site(renderable): def __init__(self, root , port=8008 , multiport=None , livereload=True , info=dict() , sources=None , generators=None , fonts=None , rect=Rect(200, 200) , watch=None , template=None , slugs="flat" , pagemod=None , favicon=None , symlink_renders=True , symlink_media=True , **kwargs ): self._watch = watch or [] self._watch.append(generics_folder / "page.j2") self.root = root self.info = info self.livereload = livereload self.sources = sources or {} self.generators = generators or {} self.template_fn = template self.slugs = slugs self.favicon = favicon self.pagemod = pagemod or (lambda x: x) self.symlink_renders = symlink_renders self.symlink_media = symlink_media self.port = port self.multiport = multiport self.generic_templates = { "page": generics_env.get_template("page.j2"), "notebook": generics_env.get_template("notebook.j2") } self.site_env = jinja2.Environment(loader=jinja2.FileSystemLoader(str(self.root / "templates"))) self.sitedir = self.root / Path("_site") self.sitedir.mkdir(exist_ok=True) if self.multiport: self.multisitedir = Path("_site") self.multisitedir.mkdir(exist_ok=True) if fonts: self.fonts = woff2s(self.sitedir / "assets/fonts", fonts, self.sitedir) for family in self.fonts: if family.embed: from base64 import b64encode for font in family.fonts: font.embedded = str(b64encode(font.woff2.read_bytes()), encoding="utf-8") else: self.fonts = [] for d in ["media", "renders"]: do_symlink = getattr(self, f"symlink_{d}") if do_symlink: src = self.root / d symlink = self.sitedir / d if src.exists() and not symlink.exists(): os.symlink(src, symlink) assetsdir = self.root / "assets" style_file = None if assetsdir.exists(): style = assetsdir / "style.css" if style.exists(): style_file = True self._watch.append(style) shutil.copytree(assetsdir, self.sitedir / "assets", dirs_exist_ok=True) if style.exists(): style2 = (self.sitedir / "assets/style.css") style2.write_text(self.mod_css(style2.read_text())) if self.info.get("style"): self.info["style"] = self.mod_css(self.info["style"]) else: if style_file is not None: self.info["has_style"] = True if self.info.get("scripts"): for script in self.info["scripts"]: if script.startswith("http"): pass else: if script.startswith("/"): ps = self.root / script[1:] else: ps = self.root / script if ps.exists(): self._watch.append(ps) else: print(ps, "does not exist") self.templates = {} for j2 in (self.root / "templates").glob("*.j2"): self._watch.append(j2) self.templates[j2.stem] = self.site_env.get_template(j2.name) for k, v in self.info.get("templates", {}).items(): self.templates[k] = string_env.from_string(v) self.pages = [] for file in (self.root / "pages").glob("**/*.md"): self._watch.append(file) page = Page.load_markdown(file, self.root, self.template_fn, self.slugs) self.pages.append(self.pagemod(page)) for file in (self.root / "pages").glob("**/*.ipynb"): # TODO could check gitignore here? self._watch.append(file) page = Page.load_notebook(file, self.root, self.generic_templates["notebook"], self.template_fn, self.slugs) self.pages.append(self.pagemod(page)) super().__init__(rect, watch=self._watch, **kwargs) def build(self): favicons = [] if self.favicon: from favicons import Favicons if isinstance(self.favicon, renderable): favicon_path = self.favicon.render_to_disk()[0] else: favicon_path = self.root / self.favicon with Favicons(favicon_path, self.sitedir) as fs: fs.generate() favicons = fs.html() version = randint(0, 100000000) year = int(datetime.now().year) build_time_utc = datetime.now(timezone.utc) rfc_822_format = "%a, %d %b %Y %H:%M:%S %z" formatted_time = build_time_utc.strftime(rfc_822_format) self.standard_data = dict( version=version , build=formatted_time , info=self.info , year=year , root=self.root , sitedir=self.sitedir , fonts=self.fonts , favicons=favicons , str=str) for k, v in self.sources.items(): self.standard_data[k] = v(self) for k, _ in self.templates.items(): if not k.startswith("_"): self.render_page(k) for page in self.pages: self.render_page(page) for k, g in self.generators.items(): g(self) if self.multiport: dst = self.multisitedir / self.root.stem dst.mkdir(exist_ok=True, parents=True) #shutil.copytree(sitedir, dst, dirs_exist_ok=True) os.system(f'rsync -r {self.sitedir}/ {dst}') for page in dst.glob("**/*.html"): html = Path(page).read_text() html = re.sub(r"=\"/", f'="/{self.root.stem}/', html) html = re.sub(r"url\('/", f"url('/{self.root.stem}/", html) Path(page).write_text(html) def mod_css(self, css): def expander(m): fontvar = f"{m[1]}-font" font = [f for f in self.fonts if f.variable_name == fontvar][0] props = eval(f"dict({m[2]})") style = Style(font.fonts[0].font, **props) fvs = ", ".join([f'"{k}" {int(v)}' for k,v in style.variations.items()]) return f'font-family: var(--{fontvar}), sans-serif;\n font-variation-settings: {fvs}' def inline_import(m): path = Path(m[1]) if path.exists(): self._watch.append(path) return f"/* start:{path} */\n\n" + self.mod_css(path.read_text()) + f"\n\n/* end:{path} */" css = re.sub(r"--([a-z]+)-font:\s?fvs\(([^\)]+)\)", expander, css) css = re.sub(r"@import \"([^\"]+)\";", inline_import, css) return css def header_footer(self, nav_links, url, page=None): nav_html = string_env.from_string(nav_template).render(nav_links=nav_links) header = self.templates.get("_header", False) if header: header = header.render({**self.standard_data, **dict(nav_links=nav_links, nav_html=nav_html, url=url, page=page)}) footer = self.templates.get("_footer", False) if footer: footer = footer.render({**self.standard_data, **dict(nav_links=nav_links, nav_html=nav_html, url=url, page=page)}) return header, footer def render_page(self, page:Page=None, data=None): nav = self.info.get("navigation", {}) header_title = self.info.get("title") if isinstance(page, str): template_name = page page = None else: template_name = page.template wrap = True if template_name == "index": path = self.sitedir / "index.html" url = "/" elif page is not None: if page.title: header_title = header_title + " | " + page.title path = page.output_path(self.sitedir) url = f"/{page.slug}" else: # TODO way to get a custom title on a bare template url = f"/{template_name}" if "." in template_name: wrap = False path = self.sitedir / f"{template_name}" else: path = self.sitedir / f"{template_name}/index.html" nav_links = [] for k, v in nav.items(): current = v == url if isinstance(v, str): classes = [] else: v, classes = v if current: classes.append("current") nav_links.append(dict(title=k , current=current , href=v , external=v.startswith("http") , classes=" ".join(classes))) header, footer = self.header_footer(nav_links, url, page) content = self.mod_css(self.templates[template_name].render({**self.standard_data, **dict(page=page, url=url), **(data or {})})) path.parent.mkdir(exist_ok=True, parents=True) #print("URL", url) if wrap: path.write_text(self.generic_templates["page"].render({ **self.standard_data, **dict( content=content , url=url , info=self.info , header=header , footer=footer , title=header_title , description=self.info.get("description"))})) else: path.write_text(content) def initial(self): kill_process_on_port_unix(self.port) maybe_run_server(self.livereload, self.port, self.sitedir) if self.multiport: kill_process_on_port_unix(self.multiport) maybe_run_server(self.livereload, self.multiport, self.multisitedir) def exit(self): kill_process_on_port_unix(self.port) if self.multiport: kill_process_on_port_unix(self.multiport) def upload(self, bucket, region="us-east-1", profile=None): args_nocache = [ "aws", "s3", "--region", region, "sync", "--cache-control", "no-cache", "--exclude", "*", "--include", "*.html", "--include", "*.xml", self.sitedir, f"s3://{bucket}" ] args_cache = [ "aws", "s3", "--region", region, "sync", self.sitedir, f"s3://{bucket}", "--exclude", ".git/*", "--exclude", "venv/*", "--exclude", "*.html" ] if profile is not None: args_nocache.extend(["--profile", profile]) args_cache.extend(["--profile", profile]) subprocess.run(args_nocache) subprocess.run(args_cache) print("/upload") ================================================ FILE: packages/coldtype-core/src/coldtype/web/templates/notebook.j2 ================================================
{% for cell in cells %} {% if cell.text is defined %}
{{ cell.text }}
{% elif cell.html is defined %}
{{ cell.html|safe }}
{% if cell.outputs is defined %}
{% for output in cell.outputs %} {% for html in output %} {{ html|safe }} {% endfor %} {% endfor %}
{% endif %} {% endif %} {% endfor %}
================================================ FILE: packages/coldtype-core/src/coldtype/web/templates/page.j2 ================================================ {{ title }} {% if atom %}{% endif %} {% for favicon in favicons %}{{ favicon|safe }} {% endfor %} {% if info["style"] %} {% elif info["has_style"] %} {% endif %}
{{ content|safe }}
{% if footer %}
{{ footer|safe }}
{% endif %}
{% if info["scripts"] %} {% for script in info["scripts"] %} {% endfor %} {% endif %} {% if info["script"] %} {% endif %} ================================================ FILE: pyproject.toml ================================================ [tool.uv.workspace] members = ["packages/*"] ================================================ FILE: release.sh ================================================ rm -rf dist cd packages/coldtype-core uv build cd ../coldtype uv build cd ../.. uvx uv-publish ================================================ FILE: run_tests.sh ================================================ ADD_UI=0 uv run python -m unittest discover test/ ADD_UI=0 uv run coldtype tests -td ================================================ FILE: scripts/inline_mixins.py ================================================ import re from pathlib import Path runon = Path("packages/coldtype-core/src/coldtype/runon") _path = runon / "_path.py" mixins = runon / "mixins" out = runon / "path.py" code = _path.read_text() all_imports = [] all_functions = [] for mixin in mixins.glob("*Mixin.py"): _code = mixin.read_text() cd = re.findall(r"class [^\:]+Mixin\(\)\:", _code)[0] imports, functions = _code.split(cd) all_imports.extend([n for n in imports.splitlines() if n]) all_functions.extend(functions.splitlines()) all_functions = "\n".join(all_functions) fs = [] last_sig = None matches = re.findall(r"[\s]{4}def[^\(]+\(self[^\)]+\)\:", all_functions) annotated = [] def write_last(src): global last_sig if last_sig: if "return self" in src: last_sig = (last_sig[:-1] + " -> \"P\":") annotated.append(last_sig) annotated.append(src) #commented_form = last_sig.replace("def ", "def _") #annotated.append(commented_form) #annotated.append(f"{commented_form}\n return self\n\n") #print(commented_form) print(last_sig.splitlines()[0]) for f in matches: before, after = all_functions.split(f) all_functions = after write_last(before) last_sig = f write_last(after) print(len(matches)) out.write_text(code .replace("# WARNING", '"""\n\n\n\n\n\n⚠️ This file was autogenerated\nby scripts/inline_mixins.py ⚠️\n\n\n\n\n\n"""') .replace("# IMPORTS", "\n".join(all_imports)) .replace("# MIXINS", "\n".join(annotated))) ================================================ FILE: scripts/keyboard_layout_converter.py ================================================ from coldtype import * from coldtype.renderer.keyboard import symbol_to_glfw, SHORTCUTS import json # layouts downloaded from https://github.com/ai/convert-layout layouts = [x for x in list(Path("~/Downloads/convert-layout-main").expanduser().glob("*.json")) if x.stem not in ["package"]] keyboards = {l.stem:json.loads(l.read_text()) for l in layouts} for layout in layouts: print(layout) ALT_LOOKUP = { "[": "", "]": "", "\\": "", } REV_ALT_LOOKUP = {v:k for k,v in ALT_LOOKUP.items()} valid_keys = set() for k, v in SHORTCUTS.items(): for n in v: if n[1] in REV_ALT_LOOKUP: valid_keys.add(REV_ALT_LOOKUP[n[1]]) else: valid_keys.add(n[1]) remaps = {} for name, keyboard in keyboards.items(): remaps[name] = {} for k, v in keyboard.items(): if k != v: if k in valid_keys: if k in ALT_LOOKUP: k = ALT_LOOKUP[k] if v in ALT_LOOKUP: v = ALT_LOOKUP[v] try: remaps[name][symbol_to_glfw(k)] = symbol_to_glfw(v) except: print("UNMAPPABLE", k, v) remaps = {k:v for k,v in remaps.items() if len(v) > 0} #print(list(remaps.keys())) import black print(black.format_str(str(remaps), mode=black.Mode(line_length=300))) #for k, remap in remaps.items(): # print(k, remap) @animation((100, 100)) def scratch(f:Frame): return None ================================================ FILE: scripts/robofont_coldtype.py ================================================ from mojo.events import addObserver, removeObserver from defconAppKit.windows.baseWindow import BaseWindowController from vanilla import FloatingWindow, CheckBox, EditText from fontTools.pens.recordingPen import RecordingPen from pathlib import Path import json DEFAULT_PATH = "~/robofont-coldtype.json" class ColdtypeSerializer(BaseWindowController): def __init__(self): self.w = FloatingWindow((300, 68), "Coldtype Serializer", minSize=(123, 200)) self.w.globalToggle = CheckBox((10, 10, -10, 20), 'serialize?', value=True) self.w.pathBox = EditText((10, 34, -10, 22), text=DEFAULT_PATH, callback=self.editTextCallback) self.path = Path(DEFAULT_PATH).expanduser().absolute() addObserver(self, "shouldDraw", "fontWillSave") self.setUpBaseWindowBehavior() self.w.open() self.writeGlyph() def editTextCallback(self, sender): self.path = Path(sender.get()).expanduser().absolute() def windowCloseCallback(self, sender): removeObserver(self, 'fontWillSave') super(ColdtypeSerializer, self).windowCloseCallback(sender) self.writeGlyph() def writeGlyph(self): glyph = CurrentGlyph() image = glyph.image print(">>>> writeGlyph", glyph, image) data_out = { "font": CurrentFont().path, "name": glyph.name, "image": dict(offset=image.offset, scale=image.scale, fileName=image.__dict__["_wrapped"]["fileName"]), "layers": {} } for g in glyph.layers: rp = RecordingPen() g.draw(rp) contours = [] for c in glyph.contours: contour = [] for pt in c.points: contour.append(dict(type=pt.type, x=pt.x, y=pt.y, smooth=pt.smooth, name=pt.name)) contours.append(contour) data_out["layers"][g.layer.name] = dict( value=rp.value, width=g.width, contours=contours) self.path.write_text(json.dumps(data_out)) def shouldDraw(self, notification): if not self.w.globalToggle.get(): return self.writeGlyph() ColdtypeSerializer() ================================================ FILE: test/drawbot/db_cli.py ================================================ from random import random from coldtype.drawbot import * from coldtype.text.reader import StyledString, Style, Font mistral = Font.Find("MistralD.otf") save = "test/drawbot/hello.pdf" with new_drawing("letter", save_to=save) as (idx, r): s = (StyledString("Hello", Style(mistral, 300)) .pens() .f(hsl(random(), s=1)) .align(r)) print(s.attrs) db.fill(*hsl(random(), l=0.3)) db.rect(*s.ambit()) s.chain(dbdraw) ================================================ FILE: test/drawbot/direct_import.py ================================================ import sys sys.path.insert(0, "/Users/robstenson/Goodhertz/coldtype") from coldtype.drawbot import * dp = (P() .define(r=Rect(100, 100), c=75) .gs("$r↗ ↘|$c|$r↓ ↙|$c|$r↖") .align(Rect(1000, 1000)) .scale(5) .f(hsl(0.45)) .ch(dbdraw)) ================================================ FILE: test/drawbot/style_test.py ================================================ import sys from pathlib import Path sys.path.insert(0, str(Path("~/Goodhertz/coldtype").expanduser())) from coldtype.drawbot import * with new_page() as r: mistral = Font.Find("MistralD.otf") s = (StSt("Hello", mistral, 300) .f(hsl(0.3, s=1)) .align(r)) with db.savedState(): db.fill(None) db.stroke(0) db.strokeWidth(2) db.rect(*s.ambit()) s.chain(dbdraw) circle = (P() .oval(r.inset(200)) .reverse() .rotate(0)) s2 = (s .copy() .zero_translate() .distribute_on_path(circle) .chain(dbdraw)) print(s.f()) print(s2.f()) db.fontSize(24) db.text("Mistral", s.ambit().inset(0, -50).ps, align="center") ================================================ FILE: test/source_file.py ================================================ from coldtype import * from .source_file_adjacent import * #INLINE @animation() def test_src_animation(f): return (StSt("COLDTYPE", "assets/ColdtypeObviously-VF.ttf", wdth=f.e("linear", 1)) .align(f.a.r)) ================================================ FILE: test/source_file_adjacent.py ================================================ hello = "world" ================================================ FILE: test/source_file_with_config.py ================================================ from coldtype import * # .coldtype WINDOW_PIN = "SW" @renderable() def test1(r): return (StSt("COLDTYPE", Font.ColdtypeObviously()) .align(r)) @renderable() def test2(r): return (StSt("COLDTYPE", Font.MutatorSans()) .align(r)) ================================================ FILE: test/test_geometry.py ================================================ import unittest from coldtype.geometry import Rect class TestGeometry(unittest.TestCase): def test_rect(self): r = Rect(0, 0, 10, 10) self.assertEqual(r.xywh(), [0, 0, 10, 10]) r = Rect(10, 10) self.assertEqual(r.xywh(), [0, 0, 10, 10]) r = Rect(10) self.assertEqual(r.xywh(), [0, 0, 10, 10]) r = Rect([0, 0, 10, 10]) self.assertEqual(r.xywh(), [0, 0, 10, 10]) r = Rect([10, 10]) self.assertEqual(r.xywh(), [0, 0, 10, 10]) r = Rect("letter") self.assertEqual(r.xywh(), [0, 0, 612, 792]) def test_interpolate(self): a = Rect(0, 0, 500, 100) b = Rect(0, 0, 200, 100) i = a.interp(0.5, b) self.assertEqual(i.w, 200+(500-200)/2) self.assertEqual(i.h, 100) self.assertEqual(i.xy(), [0, 0]) if __name__ == "__main__": unittest.main() ================================================ FILE: test/test_helpers.py ================================================ import unicodedata, unittest from pathlib import Path from coldtype.helpers import glyph_to_uni, uni_to_glyph class TestHelpers(unittest.TestCase): def test_glyph_to_uni(self): self.assertEqual(glyph_to_uni("A"), 65) self.assertEqual(uni_to_glyph(66), "B") self.assertEqual(glyph_to_uni("noondotbelow"), ord("ڹ")) if __name__ == "__main__": unittest.main() ================================================ FILE: test/test_p.py ================================================ import unittest from coldtype.runon.path import P from fontTools.pens.recordingPen import RecordingPen from coldtype.geometry import Point, Rect from coldtype.text import StSt, Font, Glyphwise, Style class TestRunonPath(unittest.TestCase): def test_init(self): r = P() self.assertIsInstance(r.v, RecordingPen) self.assertEqual(r.val_present(), False) r = P(P()) self.assertIsInstance(r.v, RecordingPen) self.assertIsInstance(r[0].v, RecordingPen) r.index(0, lambda e: e.moveTo(0, 0)) self.assertEqual(r.val_present(), False) self.assertEqual(r[0].val_present(), True) r = P(Rect(50, 50)) self.assertEqual(r.v.value[0][-1][0], (0, 0)) self.assertEqual(r.v.value[-2][-1][0], (0, 50)) self.assertEqual(r.v.value[-1][0], "closePath") r = P(P(), P()) self.assertEqual(len(r), 2) r = P([P()]*3) self.assertEqual(len(r), 3) def test_find(self): r = P( StSt("ABC", Font.MutatorSans(), 100), StSt("ABC", Font.MutatorSans(), 100)) r.î([0, 1], lambda p: p.tag("first")) r.î([1, 1], lambda p: p.tag("second")) self.assertEqual(len(r.find({"glyphName":"A"})), 2) self.assertEqual(r.find_({"glyphName":"B"}).tag(), "first") self.assertEqual(r.find_({"glyphName":"B"}, index=1).tag(), "second") self.assertEqual( r.find_({"glyphName":"B"}).tag(), r.find_(glyphName="B").tag()) self.assertNotEqual(r.find_(glyphName="B"), r.find_(glyphName="B", index=1)) r.find_(dict(glyphName="C"), lambda e: e.tag("sizzler")) self.assertEqual(r[0][-1].tag(), "sizzler") self.assertEqual(r[-1][-1].tag(), None) self.assertEqual(len(r[-1]), 3) r.find_(dict(glyphName="C"), lambda e: e.delete(), index=1) r.deblank() self.assertEqual(len(r[-1]), 2) def test_collapse(self): r = P( StSt("ABC", Font.MutatorSans(), 100), StSt("DEF", Font.MutatorSans(), 100)) self.assertEqual(len(r), 2) self.assertEqual(len(r[0]), 3) r.collapse() self.assertEqual(len(r), 6) self.assertEqual(len(r[0]), 0) def test_drawing_mixin(self): r = P() self.assertIsInstance(r._val, RecordingPen) self.assertEqual(len(r._val.value), 0) r.moveTo(0, 0) r.moveTo(Point(1, 1)) r.moveTo((2, 2)) self.assertEqual( [v[1][0] for v in r.v.value], [(0, 0), (1, 1), (2, 2)]) r = (P() .moveTo(0, 0) .lineTo(10, 10) .lineTo(Point(0, 10)) .lineTo((0, 5)) .closePath()) self.assertEqual(r.v.value, [ ('moveTo', ((0, 0),)), ('lineTo', ((10, 10),)), ('lineTo', ((0, 10),)), ('lineTo', ((0, 5),)), ('closePath', ()) ]) r = (P() .rect(Rect(10, 10, 10, 10))) self.assertEqual(r.v.value, [ ('moveTo', ((10, 10),)), ('lineTo', ((20, 10),)), ('lineTo', ((20, 20),)), ('lineTo', ((10, 20),)), ('closePath', ()) ]) r = (P() .oval(Rect(10, 10, 10, 10)) .round()) self.assertEqual(r.v.value, [ ('moveTo', [(15, 10)]), ('curveTo', [(18, 10), (20, 12), (20, 15)]), ('curveTo', [(20, 18), (18, 20), (15, 20)]), ('curveTo', [(12, 20), (10, 18), (10, 15)]), ('curveTo', [(10, 12), (12, 10), (15, 10)]), ('closePath', []) ]) def test_layout_mixin(self): r = StSt("AB C", Font.MutatorSans(), 100) self.assertEqual(r[2].data("glyphName"), "space") self.assertAlmostEqual(r[2].ambit().x, 84.3, 2) self.assertFalse(r[2].bounds().nonzero()) r.translate(100, 0) self.assertAlmostEqual(r[2].ambit().x, 184.3, 2) def test_fx_mixin(self): r = StSt("ABC", Font.MutatorSans(), 100) self.assertEqual(r[0].data("glyphName"), "A") r = P(P(Rect(0, 0, 100, 100))) self.assertEqual(r.ambit().y, 0) r.translate(0, 100) self.assertEqual(r.ambit().y, 100) r.layer(1, 1) r.translate(0, 100) self.assertEqual(r.ambit().y, 200) r = StSt("B", Font.MutatorSans(), 100) def test_glyphwise(self): r = Glyphwise("ABC", lambda _: Style(Font.MuSan(), 100)) self.assertIsInstance(r, P) r = Glyphwise("ABC\nDEF", lambda _: Style(Font.MuSan(), 100)) self.assertIsInstance(r, P) r = Glyphwise("ABC\nDEF", lambda g: [Style(Font.MuSan(), 100), dict(wdth=g.e)]) self.assertIsInstance(r, P) def test_mods(self): r = StSt("ABC", Font.MuSan(), 500, ro=1) self.assertEqual( r.index([1, 2]).v.value, r.index(1).index(2).v.value) before_rotate = r.index([1, 2]).bounds() before_frame = r.index([1]).ambit(tx=0, ty=0) r.find_(dict(glyphName="B"), lambda p: p.î(2, lambda c: c.rotate(-5))) self.assertNotEqual(before_rotate, r.î([1, 2]).bounds()) self.assertEqual(before_frame, r.î(1).ambit()) def test_empty(self): r = P() r.data(frame=Rect(0, 0, 100, 100)) r.collapse() r.translate(10, 10) #print(r) if __name__ == "__main__": unittest.main() ================================================ FILE: test/test_pens.py ================================================ import unittest from random import Random from coldtype.geometry import Rect, Point from coldtype.runon.path import P from coldtype.color import hsl, rgb from coldtype.pens.drawablepen import DrawablePenMixin from coldtype.renderer.reader import SourceReader from coldtype.text.composer import StSt, Font from coldtype.fx.chainable import Chainable co = Font.Cacheable("assets/ColdtypeObviously-VF.ttf") mutator = Font.Cacheable("assets/MutatorSans.ttf") class TestPens(unittest.TestCase): def test_gs(self): r = Rect(0, 0, 100, 100) dps = P() dp = (P() .declare(r:=r) .m(r.pnw).l(r.pne).bxc(r.psw, "se") .ep()) self.assertEqual(len(dp.v.value), 4) self.assertEqual(dp.v.value[-2][-1][0], Point(100, 35)) self.assertEqual(dp.v.value[-1][0], "endPath") self.assertEqual(dp.unended(), False) dps.append(P([dp])) self.assertEqual(len(dps.tree().splitlines()), 4) self.assertEqual(dps.tree().splitlines()[-1], " | - <®:P:RecordingPen(4mvs)>") def test_gs_arrowcluster(self): r = Rect(100, 100) dp = (P().m(r.pnw).l(r.pne).l(r.pse).ep()) self.assertEqual(len(dp.v.value), 4) self.assertEqual(dp.v.value[0][-1][0], Point(0, 100).xy()) self.assertEqual(dp.v.value[1][-1][0], Point(100, 100).xy()) self.assertEqual(dp.v.value[2][-1][0], Point(100, 0).xy()) def test_gs_relative_moves(self): r = Rect(100, 100) dp = (P().m(r.pnw).l(r.pnw.o(50,-50)).l(r.pnw.o(0,-50)).ep()) self.assertEqual(len(dp.v.value), 4) self.assertEqual(dp.v.value[0][-1][0], Point(0, 100).xy()) self.assertEqual(dp.v.value[1][-1][0], Point(50, 50).xy()) self.assertEqual(dp.v.value[2][-1][0], Point(0, 50).xy()) def test_reverse(self): r = Rect(100, 100) dp = (P().m(r.pnw).l(r.pne).l(r.pse).ep()) p1 = dp.v.value[0][-1] p2 = dp.reverse().v.value[-2][-1] self.assertEqual(p1, p2) def test_transforms(self): dp = (P(Rect(100, 100)) .data(frame=Rect(100, 100)) .align(Rect(200, 200))) self.assertEqual(dp.data("frame").mxx, 150) self.assertEqual(dp.v.value[-2][-1][-1][0], 50) self.assertEqual( dp.copy().rotate(45).round().v.value, dp.copy().rotate(360+45).round().v.value) self.assertEqual(dp.copy().scale(2).ambit().w, 200) def test_pens_ambit(self): dps = (P([ P(Rect(50, 50)), P(Rect(100, 100, 100, 100))]) #.print(lambda x: x.tree()) ) ram = dps.ambit() self.assertEqual(ram, Rect(0, 0, 200, 200)) moves = [] dps.walk(lambda p, pos, _: moves.append([p, pos])) self.assertEqual(moves[0][0], dps) self.assertEqual(moves[0][1], -1) self.assertEqual(moves[1][1], 0) def test_remove_blanks(self): dps = (P([ P(Rect(50, 50)), P() ])) self.assertEqual(len(dps), 2) dps.deblank() self.assertEqual(len(dps), 1) def test_collapse(self): rr = Rect(100, 100) r = P([ P([P([P().rect(rr)])]), P([P().rect(rr)]), ]) self.assertIsInstance(r[0], P) self.assertIsInstance(r[0][0], P) r.collapse() self.assertIsInstance(r[0], P) self.assertIsInstance(r[1], P) r = P([ P([P([P().rect(rr)])]), P([P().rect(rr)]), ]) r2 = r.copy().collapse() self.assertEqual(len(r), 2) self.assertIsInstance(r[0], P) self.assertIsInstance(r[0][0], P) self.assertIsInstance(r2[0], P) self.assertIsInstance(r2[1], P) r = P([ P([P([P()])]), P([P()]), ]) r2 = r.copy().collapse() self.assertEqual(len(r), 2) self.assertEqual(len(r2), 0) r = P([ P([P([P()])]), P([P()]), ]) r2 = r.copy().collapse(deblank=False) self.assertEqual(len(r), 2) self.assertEqual(len(r2), 2) def test_find(self): dps = P([ P([P([P().tag("find-me").f(hsl(0.9))])]), P().tag("not-me"), P([P().tag("find-me").f(hsl(0.3))])]) self.assertEqual(dps.find("find-me")[1].f().h/360, 0.9) self.assertAlmostEqual(dps.find("find-me")[0].f().h/360, 0.3) def test_cond(self): dps = (P([ (P().cond(True, lambda p: p.f(rgb(1, 0, 0))))])) self.assertEqual(dps[0].f().r, 1) def _build(condition): return (P([ (P().cond(condition, lambda p: p.f(rgb(0, 0, 1)), lambda p: p.f(rgb(1, 0, 0))))])) self.assertEqual(_build(True)[0].f().b, 1) self.assertEqual(_build(False)[0].f().r, 1) def test_alpha(self): dps = (P([ (P([ (P().alpha(0.5)) ]).alpha(0.5)) ]).alpha(0.25)) def walker(p, pos, data): if pos == 0: self.assertEqual(data["alpha"], 0.0625) elif pos == 1 and data["depth"] == 0: self.assertEqual(data["alpha"], 0.25) elif pos == 1 and data["depth"] == 1: self.assertEqual(data["alpha"], 0.125) dps.walk(walker) def test_visibility(self): dps = (P([ (P([ (P().visible(1).tag("visible")), (P().visible(0).tag("invisible")) ])) ])) def walker(p, pos, data): nonlocal visible_pen_count if pos == 0: visible_pen_count += 1 visible_pen_count = 0 dps.walk(walker, visible_only=True) self.assertEqual(visible_pen_count, 1) visible_pen_count = 0 dps.walk(walker, visible_only=False) self.assertEqual(visible_pen_count, 2) visible_pen_count = 0 dps[0][0].visible(0) dps.walk(walker, visible_only=True) self.assertEqual(visible_pen_count, 0) def test_style(self): src = """ from coldtype import * def two_styles(r): return (P() .oval(r.inset(50).square()) .f(hsl(0.8)) .attr("alt", fill=hsl(0.3))) @renderable() def no_style_set(r): return two_styles(r) @renderable(style="alt") def style_set(r): return two_styles(r) def lattr_styles(r): return (P() .oval(r.inset(50).square()) .f(hsl(0.5)).s(hsl(0.7)).sw(5) .lattr("alt", lambda p: p.f(hsl(0.7)).s(hsl(0.5)).sw(15))) @renderable() def lattr_no_style(r): return lattr_styles(r) @renderable(style="alt") def lattr_style_set(r): return lattr_styles(r) """ sr = SourceReader(None, code=src) rs = sr.frame_results(0) sr.unlink() self.assertNotEqual( rs[0][1][0].attr(rs[0][0].style, "fill"), rs[1][1][0].attr(rs[1][0].style, "fill")) self.assertNotEqual( rs[2][-1][0].attr(rs[2][0].style, "fill"), rs[3][-1][0].attr(rs[3][0].style, "fill")) self.assertEqual(rs[2][-1][0].attr(rs[2][0].style, "strokeWidth"), 5) self.assertEqual(rs[3][-1][0].attr(rs[3][0].style, "strokeWidth"), 15) dpm = DrawablePenMixin() dpm.dat = rs[3][-1][0] attrs = [x for _, x in list(dpm.findStyledAttrs(rs[3][0].style))] self.assertEqual(len(attrs), 2) self.assertEqual(attrs[1][1].get("weight"), 15) def test_subsegmenting(self): f1 = Font.Cacheable("assets/ColdtypeObviously_BlackItalic.ufo") shape = (StSt("C", f1, 1000, wght=0.5)[0] .explode()[0]) self.assertAlmostEqual( shape.length()/2, shape.copy().subsegment(0, 0.5).length(), delta=1) self.assertAlmostEqual( shape.length(), shape.copy().subsegment(0, 1).length(), delta=1) shape1 = (StSt("D", f1, 1000, wght=0.5)[0] .explode()[0]) shape2 = shape1.copy().fully_close_path() self.assertLess(shape1.length(), shape2.length()) self.assertAlmostEqual( shape2.length()/2, shape2.copy().subsegment(0, 0.5).length(), delta=1) def test_explode(self): r = Rect(1000, 500) o1 = (StSt("O", co, 500, wdth=1).pen()) o2 = (StSt("O", co, 500, wdth=1) .pen() .explode() .index(1, lambda p: p.rotate(90)) .implode().f(hsl(0.3)).align(r)) self.assertEqual( o1.explode()[0].ambit().w, o2.explode()[0].ambit().w) self.assertAlmostEqual( o1.explode()[1].ambit().h, o2.explode()[1].ambit().w) def test_chain(self): def c1(a): def _c1(p:P): return [a] return Chainable(_c1) def c2(a): def _c2(p:P): p.data(hello=a) return None return Chainable(_c2) p1 = P() | c1(1) self.assertEqual(p1, [1]) p2 = P() | c2("chain") self.assertTrue(isinstance(p2, P)) self.assertEqual(p2.data("hello"), "chain") if __name__ == "__main__": unittest.main() ================================================ FILE: test/test_pens_rendered.py ================================================ import unittest from coldtype.pens.skiapen import SkiaPen from pathlib import Path from coldtype.color import hsl from coldtype.geometry import Rect from coldtype.text.composer import StSt, Font from coldtype.runon.path import P from PIL import Image import imagehash import contextlib co = Font.Cacheable("assets/ColdtypeObviously-VF.ttf") renders = Path("test/renders/skia") renders.mkdir(parents=True, exist_ok=True) def hash_img(path): if path.exists(): return ( imagehash.colorhash(Image.open(path)), imagehash.average_hash(Image.open(path))) else: return -1 @contextlib.contextmanager def test_image(test:unittest.TestCase, path, rect=Rect(1000, 500)): img = (renders / path) hash_before = hash_img(img) if img.exists(): img.unlink() yield(img, rect) hash_after = hash_img(img) #test.assertEqual(hash_after, hash_before) #test.assertEqual(img.exists(), True) class TestPensRendered(unittest.TestCase): def test_skia_png(self): with test_image(self, "test_skia.png") as (i, r): dp = ((ß:=P()) .declare(nx:=100, a:=r.inset(100, 0).subtract(200, "N")) .m(a.psw) .bxc(a.pn, a.pnw.o(nx, 0), 65) .bxc(a.pse, a.pne.o(-nx, 0), 65) .ep() .fssw(-1, 0, 4) .ups() .append(StSt("Coldtype Cdelopty".upper(), co, 100, wdth=0.5) .pens() .distribute_on_path(ß[0], center=-5) .f(hsl(0.5))) .align(r)) SkiaPen.Precompose(dp, r, disk=str(i)) self.assertEqual(len(dp), 2) self.assertEqual(type(dp), P) if __name__ == "__main__": unittest.main() ================================================ FILE: test/test_runon.py ================================================ import unittest from coldtype.runon.runon import * #INLINE class TestRunon(unittest.TestCase): def test_init(self): r = Runon(1) self.assertEqual(r.v, 1) r = Runon(Runon(1)) self.assertEqual(r.v, None) self.assertEqual(len(r), 1) r = Runon(Runon(1), Runon(2)) self.assertEqual(r.v, None) self.assertEqual(len(r), 2) self.assertEqual(r[0].v, 1) self.assertEqual(r[1].v, 2) r = Runon([Runon(1), Runon(2)]) self.assertEqual(r.v, None) self.assertEqual(len(r), 2) self.assertEqual(r[0].v, 1) self.assertEqual(r[1].v, 2) r = Runon(3, 2, 1) self.assertEqual(len(r), 3) self.assertEqual(r[0].v, 3) self.assertEqual(r[-1].v, 1) r.reverse() self.assertEqual(r[0].v, 1) self.assertEqual(r[-1].v, 3) r = Runon( Runon(1).data(hi="word"), Runon(2).tag("oy")) self.assertEqual(len(r), 2) self.assertEqual(r[0].data("hi"), "word") self.assertEqual(r[1].data("hi"), None) self.assertEqual(r[1].tag(), "oy") #self.assertEqual(r[2], None) self.assertEqual(len(r.find("oy")), 1) self.assertEqual(len(r.find_("oy")), 0) self.assertEqual(r.find_("oy").tag(), "oy") r.mapv(lambda p: p.update(p.v+1)) self.assertEqual(r[0].v, 2) self.assertEqual(r[1].v, 3) r.filterv(lambda p: p.v == 3) self.assertEqual(r[0].v, 3) self.assertEqual(r[0].tag(), "oy") r.insert(0, Runon(4)) self.assertEqual(r[0].v, 4) self.assertEqual(r.sum(), [4, 3]) self.assertEqual(len(r.find(lambda p: p.v == 3)), 1) self.assertEqual(len(r.find(lambda p: p.v == 4)), 1) self.assertEqual(len(r.find(lambda p: p.v == 5)), 0) r.insert([0, 0], Runon(6)) r.insert([0, 0], Runon(5)) self.assertEqual(r[0].sum(), [4, 5, 6]) r.data(hello="world") r.index([0, 0], lambda e: e.attr(fill=1)) r_copy = r.copy() self.assertEqual(r_copy[0][0].attr("fill"), 1) r.index([0, 0], lambda e: e.attr(fill=2)) r_copy.index([0, 0], lambda e: e.attr(fill=3)) self.assertEqual(r[0][0].attr("fill"), 2) self.assertEqual(r_copy[0][0].attr("fill"), 3) self.assertEqual(r_copy.data("hello"), "world") self.assertEqual(r_copy[-1].tag(), "oy") r_rev1 = r.copy().reverse(recursive=0) r_rev2 = r.copy().reverse(recursive=1) self.assertEqual(r_rev1[-1].sum(), [4, 5, 6]) self.assertEqual(r_rev2[-1].sum(), [4, 6, 5]) r.insert([0, 0, 0], Runon(10)) utags = [] def walker(_, pos, data): if pos >= 0: utags.append(data.get("utag")) r.walk(walker) self.assertEqual(utags, ['0_0_0', '0_0', '0_1', '0', '1', 'ROOT']) self.assertEqual(r.index(0).v, 4) r.index(0, lambda p: p.update(40)) self.assertEqual(r.index(0).v, 40) r.index([0, 0, 0], lambda p: p.update(20)) self.assertEqual(r.index([0, 0, -1]).v, 20) r.index([0, 0, -1], lambda p: p.update(200)) self.assertEqual(r.index([0, 0, -1]).v, 200) els = r.indices([-1, [0, 0, -1]]) self.assertEqual(len(els), 2) self.assertEqual(els[0].v, 3) self.assertEqual(els[0].tag(), "oy") self.assertEqual(els[1].v, 200) r = Runon(1) utags = [] r.walk(walker) self.assertEqual(utags, ["ROOT"]) def test_attr(self): r = Runon(Runon(1).attr(q=2)) self.assertEqual(r.attr("q"), None) self.assertEqual(r[0].attr("q"), 2) r.index(0, lambda p: p.attr(q=3)) self.assertEqual(r[0].attr("q"), 3) r[0].lattr("alt", lambda p2: p2.attr(q=4)) self.assertEqual(r[0].attr("q"), 3) self.assertEqual(r[0].attr("alt", "q"), 4) def test_alpha(self): r = Runon(Runon(1).alpha(0.5).tag("leaf")) r.alpha(0.5).tag("root") alphas = {} def walker(el, pos, data): if pos >= 0: alphas[el.tag()] = data.get("alpha") r.walk(walker) self.assertEqual(alphas["root"], 0.5) self.assertEqual(alphas["leaf"], 0.25) self.assertEqual(r[0].alpha(), 0.5) def test_logic(self): r = Runon(Runon(1), Runon(2)) r.cond(True, lambda p: p.data(a="b", c="d")) r.cond(False, lambda p: p, lambda p: p.data(x="z")) self.assertEqual(r.data("a"), "b") self.assertEqual(r.data("c"), "d") self.assertEqual(r.data("x"), "z") def test_layers(self): r = Runon(5) r.layer(3) self.assertEqual(len(r), 3) r.layer(2) self.assertEqual(len(r), 2) self.assertEqual(len(r[0]), 3) r = Runon(5) self.assertEqual(r.v, 5) self.assertEqual(len(r), 0) self.assertEqual(bool(r), True) r.layerv(2) self.assertEqual(r.v, None) self.assertEqual(len(r), 2) self.assertEqual(len(r[0]), 0) self.assertEqual(bool(r), True) r.layerv(3) self.assertEqual(r.v, None) self.assertEqual(len(r), 2) self.assertEqual(len(r[0]), 3) r = Runon(1) self.assertEqual(r.v, 1) self.assertEqual(r.depth(), 0) r.layerv(lambda p: p.v + 2) self.assertEqual(r.v, None) self.assertEqual(r[0].v, 3) self.assertEqual(r.depth(), 1) r.layerv(lambda p: p.v + 2) self.assertEqual(r[0].v, None) self.assertEqual(r[0][0].v, 5) self.assertEqual(r.depth(), 2) r.layerv(lambda p: p.v + 2, lambda p: p.v + 3) self.assertEqual(r[0][0].v, None) self.assertEqual(r[0][0][0].v, 7) self.assertEqual(r[0][0][-1].v, 8) self.assertEqual(r.depth(), 3) r.collapse() self.assertEqual(len(r), 2) self.assertEqual(r[0].v, 7) self.assertEqual(r[-1].v, 8) r = Runon(1, 2, 3) r[0].data(hello="world") r.layerv(1, lambda e: e.update(e.v+1)) self.assertEqual(r[0].data("hello"), None) r = Runon(1) r.ups() self.assertEqual(r[0].v, 1) r.ups() self.assertEqual(r[0].v, None) self.assertEqual(r[0][0].v, 1) r.ups() r.insert(0, Runon(2)) self.assertEqual(r[0].v, 2) self.assertEqual(r[1][0].v, None) self.assertEqual(r[1][0][0].v, 1) r.ups() self.assertEqual(len(r), 1) self.assertEqual(len(r[0]), 2) def test_collapse(self): r = Runon(Runon(Runon(1, 2), 3), Runon(4, 5)) r.collapse() self.assertEqual(r.sum(), [1, 2, 3, 4, 5]) r.reverse() self.assertEqual(r.sum(), [5, 4, 3, 2, 1]) r = Runon(Runon(1, 2, 3), Runon(4, 5, 6)) r.collapse() self.assertEqual(r.sum(), [1, 2, 3, 4, 5, 6]) def test_chain(self): def c(a): def _c(ru): return ru.update(a) return _c r = Runon(1) self.assertEqual(r.v, 1) r.chain(c(5)) self.assertEqual(r.v, 5) r.layerv(2) r.index(1, lambda e: e.update(e.v*2)) self.assertEqual(r[0].v, 5) self.assertEqual(r[-1].v, 10) r.mapv(lambda e: e.update(e.v*2)) self.assertEqual(r[0].v, 10) self.assertEqual(r[-1].v, 20) def c2(): def _c(ru): return ru.update(10) return _c r = Runon(1) self.assertEqual(r.v, 1) r.chain(c2) self.assertEqual(r.v, 10) # variant syntax r = Runon(1, 2, 3) r / (lambda p: p.update(p.v+1)) self.assertEqual(r.sum(), [2, 3, 4]) def ch(a): def _ch(ro): ro / (lambda p: p.update(p.v + a)) return _ch r = Runon(1, 2, 3) r | ch(2) self.assertEqual(r.sum(), [3, 4, 5]) r = Runon(1, 2, 3) r - ch(2) self.assertEqual(r.sum(), [1, 2, 3]) def test_inter(self): r = Runon(1, 2, 3) self.assertEqual(len(r), 3) r.interpose(Runon(10)) self.assertEqual(len(r), 5) r = Runon(1, 2, 3) self.assertEqual(len(r), 3) self.assertEqual(len(r[0]), 0) r.layerv(1, lambda e: e.update(e.v+1)) self.assertEqual(len(r), 3) self.assertEqual(len(r[0]), 2) self.assertEqual(r[0][0].v, 1) self.assertEqual(r[0][1].v, 2) r = Runon(3, 2, 1) r.split(2) self.assertEqual(len(r), 2) self.assertEqual(r.sum(), [3, 1]) r = Runon(1, 2, 3) r.split(lambda e: e.v == 2) self.assertEqual(len(r), 2) self.assertEqual(len(r[0]), 1) self.assertEqual(len(r[1]), 1) self.assertEqual(r.sum(), [1, 3]) r = Runon(1, 2, 3) r.split(lambda e: e.v == 2, -1) self.assertEqual(len(r), 2) self.assertEqual(len(r[0]), 2) self.assertEqual(len(r[1]), 1) self.assertEqual(r.sum(), [1, 2, 3]) r = Runon(1, 2, 3) r.split(lambda e: e.v == 2, 1) self.assertEqual(len(r), 2) self.assertEqual(len(r[0]), 1) self.assertEqual(len(r[1]), 2) self.assertEqual(r.sum(), [1, 2, 3]) def test_enumerate(self): r = Runon().enumerate(range(0, 5), lambda en: Runon((en.el+1)*10)) self.assertEqual(r.sum(), [10, 20, 30, 40, 50]) r = Runon().enumerate(range(0, 5), lambda en: Runon(en.e)) self.assertEqual(r.sum(), [0, 0.25, 0.5, 0.75, 1]) r = Runon().enumerate(zip(["A", "B", "C"], [1, 2, 3]), lambda en: Runon(f"{en.el[0]}{en.el[1]}")) self.assertEqual(r[0].v, "A1") self.assertEqual(r[-1].v, "C3") def test_add(self): r = Runon(1, 2, 3) r += Runon(4) self.assertEqual(r.sum(), [1, 2, 3, 4]) r = Runon(1, 2, 3) r2 = r + 4 self.assertEqual(r.sum(), [1, 2, 3]) self.assertEqual(r2.sum(), [1, 2, 3, 4]) self.assertEqual(r2[0].sum(), [1, 2, 3]) self.assertEqual(r2[1].sum(), [4, 4]) def test_index(self): r = Runon(1, 2, 3) r.index(0, lambda e: e.tag("one")) self.assertEqual(r[0].tag(), "one") def test_find(self): r = Runon(Runon(1, 2), Runon(1, 2, 3)) r.index([0, 1], lambda e: e.tag("alpha")) r.index([1, 1], lambda e: e.tag("beta")) self.assertEqual( r.find_(lambda e: e.v == 2).tag(), "alpha") self.assertEqual( r.find_(lambda e: e.v == 2, index=1).tag(), "beta") def test_append_insert(self): # should normalize and ignore None r = Runon(1, 2, 3) r.insert(0, 10) self.assertEqual(r[0].v, 10) r.insert(0, None) self.assertEqual(r[0].v, 10) self.assertEqual(r[-1].v, 3) r.append(None) self.assertEqual(r[-1].v, 3) r += None self.assertEqual(r[-1].v, 3) r = Runon(1, 2, 3, None) self.assertEqual(len(r), 3) self.assertEqual(r[-1].v, 3) r = Runon(1, 2, 3, Runon(None)) self.assertEqual(len(r), 4) self.assertEqual(r[-1].v, None) def test_tree(self): r = Runon(1, 2, Runon(11, Runon(21, 22), 13), 3) rt1 = r.tree().split("\n") self.assertTrue(" | | - <®::int(22)>" in rt1) self.assertEqual(len(rt1), 11) rt2 = r.tree(v=False).split("\n") self.assertFalse(" | | - <®::int(22)>" in rt2) self.assertEqual(len(rt2), 4) r.î([2], lambda e: e.data( a="b"*20, c="d"*20, e="f"*20, g="h"*20, i="j"*20, k="l"*20)) self.assertEqual(r.tree(), """ <®::/4...> - <®::int(1)> - <®::int(2)> | <®::/3... {a=bbbbbbbbbbbbbbbbbbbb,c=dddddddddddddddddddd,e=ffffffffffffffffffff,g=hhhhhhhhhhhhhhhhhh hh,i=jjjjjjjjjjjjjjjjjjjj,k=llllllllllllllllllll}> | - <®::int(11)> | | <®::/2...> | | - <®::int(21)> | | - <®::int(22)> | - <®::int(13)> - <®::int(3)>""") if __name__ == "__main__": unittest.main() ================================================ FILE: test/test_syntax_mods.py ================================================ import unittest from pathlib import Path from coldtype.renderable import renderable, Action from coldtype.renderer.reader import SourceReader test_src = """ from coldtype import * @renderable() def test_src_function(r): return (StSt("CDELOPTY", "assets/ColdtypeObviously-VF.ttf") .align(r.inset(50)) .f(hsl(0.5, 0.7, 0.9))) @animation() def test_src_animation(f): return (StSt("CDELOPT", "assets/ColdtypeObviously-VF.ttf", wdth=f.e("linear", 1)) .align(f.a.r)) """ class TestSyntaxMods(unittest.TestCase): def setUp(self) -> None: self.sr = SourceReader(None, test_src) self.sr2 = SourceReader(runner="special") return super().setUp() def tearDown(self) -> None: self.sr.unlink() self.sr2.unlink() return super().tearDown() def test_empty_reader(self): self.assertEqual(self.sr2.filepath, None) self.assertEqual(self.sr2.codepath, None) self.sr2.reset_filepath("test/source_file.py") self.assertEqual(self.sr2.filepath.stem, "source_file") self.assertEqual(len(self.sr2.renderables()), 1) self.assertEqual(self.sr2.program["__RUNNER__"], "special") self.assertEqual(self.sr2.program["hello"], "world") def test_source_with_config(self): sr = SourceReader("test/source_file_with_config.py") sr.unlink() self.assertEqual(sr.filepath.stem, "source_file_with_config") self.assertEqual(len(sr.renderables()), 2) self.assertEqual(sr.config.window_pin, "SW") def test_frame_read(self): a1, _ = self.sr.frame_results(1) _, res = a1 self.assertEqual(len(res[0]), 8) def test_syntax_mods(self): return sr = self.sr sr.reload(output_folder_override="test/renders") mod_src = sr.codepath.read_text() self.assertIn(".align(r.inset(50))", test_src) self.assertNotIn(".align(r.inset(50))", mod_src) self.assertIn(".noop()", mod_src) self.assertEqual(sr.program["__RUNNER__"], "default") renderables = sr.renderables() self.assertEqual(len(renderables), 2) self.assertEqual(renderables[0].codepath, sr.codepath) r1:renderable = renderables[0] r1_passes = r1.passes(None, None, []) r1p1_result = r1.run_normal(r1_passes[0]) self.assertEqual(r1p1_result[3].glyphName, "L") r2:renderable = renderables[1] r2_passes = r2.passes(Action.RenderAll, None, []) for idx, rp in enumerate(r2_passes): rel = rp.output_path.relative_to(Path.cwd() / "test/renders") self.assertEqual( "test_src_animation_{:04}.png".format(idx), str(rel)) # verify that the width of the 'C' is increasing over the first half of the animation r2p2_result = r2.run_normal(r2_passes[1]) r2p3_result = r2.run_normal(r2_passes[2]) r2p4_result = r2.run_normal(r2_passes[3]) self.assertLess( r2p2_result[0].ambit().w, r2p3_result[0].ambit().w) self.assertLess( r2p3_result[0].ambit().w, r2p4_result[0].ambit().w) renderables = sr.renderables(viewer_solos=[1]) self.assertEqual(len(renderables), 1) self.assertEqual(renderables[0].name, "test_src_animation") # indexes over len wrap-around renderables = sr.renderables(viewer_solos=[2]) self.assertEqual(len(renderables), 1) self.assertEqual(renderables[0].name, "test_src_function") renderables = sr.renderables(function_filters=[r".*_function"]) self.assertEqual(len(renderables), 1) self.assertEqual(renderables[0].name, "test_src_function") renderables = sr.renderables(function_filters=[r".*_animation"]) self.assertEqual(len(renderables), 1) self.assertNotEqual(renderables[0].name, "test_src_function") renderables = sr.renderables(class_filters=[r".*asdf$"]) self.assertEqual(len(renderables), 0) renderables = sr.renderables(class_filters=[r".*ation$"]) self.assertEqual(len(renderables), 1) self.assertEqual(renderables[0].__class__.__name__, "animation") # when the pattern matches nothing, all renderables returned renderables = sr.renderables(function_filters=[r".*_should_be_nothing"]) self.assertEqual(len(renderables), 2) #sr.reload(test_src.replace("-.align(", ".align(")) mod_src = sr.codepath.read_text() self.assertNotIn(".noop()", mod_src) self.assertIn(".align(r.inset(50))", mod_src) prev_codepath = sr.codepath prev_filepath = sr.filepath test_source_file = Path("test/source_file.py") sr.reset_filepath("test/source_file.py") self.assertEqual(sr.filepath.absolute(), test_source_file.absolute()) self.assertNotEqual(sr.codepath, prev_codepath) self.assertTrue(not prev_filepath.exists()) self.assertTrue(not prev_codepath.exists()) self.assertTrue(sr.codepath.exists()) sr.unlink() self.assertTrue(not sr.codepath.exists()) self.assertTrue(test_source_file.exists()) sr.reset_filepath("test/source_file") self.assertEqual(sr.filepath.suffix, ".py") self.assertEqual(sr.filepath, test_source_file.absolute()) if __name__ == "__main__": unittest.main() ================================================ FILE: test/test_time.py ================================================ import unittest from coldtype.timing.nle.ascii import AsciiTimeline from coldtype.renderable.animation import animation, Frame from coldtype.renderer.reader import SourceReader from coldtype.timing.timeable import Timeable from coldtype.timing.timeline import Timeline from coldtype.text import Font, Style, Rect at = AsciiTimeline(1, """ | [a ] [b ] [c ] ] [d """) at2 = AsciiTimeline(1, """ 1 2 3 [four ] """) at3 = AsciiTimeline(1, 24, """ < .style1 *Oh, hello. • *This some is t +x +t • """) at4 = AsciiTimeline(1, 30, """ < [0 ] [0 ] [0 ] [0 ] """) class TestTime(unittest.TestCase): def test_ascii_timeline_1(self): self.assertEqual(at.duration, 36) self.assertEqual(at["a"][0].start, 0) self.assertEqual(at["b"][0].end, 21) self.assertEqual(at["c"][0].start, 16) self.assertEqual(at["d"][0].end, 41) self.assertEqual(len(at.timeables), 4) self.assertEqual(at.ki("a", 0).e(), 0) self.assertAlmostEqual(at.ki("a", 4).e(loops=0), 0.0347, 3) self.assertAlmostEqual(at.ki("a", 4).e(loops=1), 0.8990, 3) self.assertNotEqual(at.ki("b", 20).e(loops=0, to1=0), 1) self.assertEqual(at.ki("b", 20).e(loops=0, to1=1), 1) self.assertEqual(at[1].start, -1) self.assertEqual(at.ki(1, 30).t.start, -1) self.assertEqual(at.ki("b", 8).io(5), 0) self.assertEqual(at.ki("b", 8+5).io(5), 1) def test_ascii_timeline_2(self): self.assertEqual(at2.duration, 31) self.assertIsInstance(at2["1"][0], Timeable) self.assertEqual(at2[1][0].duration, 0) self.assertEqual(at2.ki("2", 8).adsr(), 1) self.assertEqual(at2.ki("2", 0).adsr(), 0) self.assertAlmostEqual(at2.ki("2", 8+18).adsr([5, 20]), 0.001, 3) self.assertAlmostEqual(at2.ki("2", 8+18).adsr([5, 20], ["eei", "qeio"]), 0.0055, 3) self.assertEqual(at2.ki("2", 8+3).adsr([5, 5, 20], ["eei", "qeio"], rng=(10, -10)), -10) at2.hold(8+18) self.assertAlmostEqual(at2.ki("2").adsr([5, 20], ["eei", "qeio"]), 0.0055, 3) def test_ascii_timeline_3(self): from coldtype.timing.sequence import ClipTrack, ClipGroup, Clip, ClipType self.assertEqual(at3.duration, 52) self.assertEqual(at3.fps, 24) ct = at3.words self.assertIsInstance(ct, ClipTrack) self.assertIsInstance(ct.styles, Timeline) self.assertEqual(len(ct.styles), 1) self.assertEqual(ct.styles[0].name, "style1") cg:ClipGroup = ct.currentGroup(0) self.assertEqual(len(cg.clips), 3) self.assertEqual(cg.clips[0].type, ClipType.ClearScreen) self.assertEqual(cg.clips[0].text, "Oh,") self.assertEqual(cg.clips[1].type, ClipType.Isolated) self.assertEqual(cg.clips[1].text, "hello.") self.assertEqual(cg.clips[-1].type, ClipType.EndCap) self.assertEqual(cg.clips[-1].text, "") cg:ClipGroup = ct.currentGroup(16) self.assertEqual(cg.duration, 0) cg:ClipGroup = ct.currentGroup(19) self.assertEqual(cg.duration, 0) cg:ClipGroup = ct.currentGroup(20) self.assertNotEqual(cg.duration, 0) cg:ClipGroup = ct.currentGroup(28) self.assertEqual(len(cg.clips), 7) self.assertEqual(cg.clips[0].type, ClipType.ClearScreen) self.assertEqual(cg.clips[-1].type, ClipType.EndCap) self.assertEqual(cg.clips[-1].text, "") def styler(c): if "style1" in c.styles: return c.text, Style(Font.RecursiveMono(), 150) else: return c.text.upper(), Style(Font.MutatorSans(), 150) cgp = cg.pens(Frame(28, at3), styler, Rect(1080, 1080)) self.assertEqual(len(cgp), 1) self.assertEqual(len(cgp[0]), 4) self.assertEqual(cgp[0].data("line_text"), "This is some txt") self.assertEqual(cgp[0][0][0].data("position"), 1) self.assertEqual(cgp[0][1][0].data("position"), 0) self.assertEqual(cgp[0][2][0].data("position"), -1) self.assertEqual(cgp[0][1][0][-2].glyphName, "S.closed") def test_ascii_timeline_ec(self): self.assertEqual(at4.duration, 36) self.assertEqual(at4.ki(0, 0).ec("l", rng=(0, 90)), 0) self.assertEqual(at4.ki(0, 10).ec("l", rng=(0, 90)), 90) self.assertEqual(at4.ki(0, 35).ec("l", rng=(0, 90)), 360) def test_animation(self): src = "examples/animations/alphabet.py" sr = SourceReader(src).unlink() anim:animation = sr.renderables()[0] self.assertEqual(anim.duration, 26) for i in range(0, anim.duration): p = anim.passes(None, None, indices=[i])[0] res = anim.run_normal(p) gn = chr(65+i) if gn == "I": gn = "I.narrow" self.assertEqual(res[1][0][0].glyphName, gn) if i == 13: self.assertAlmostEqual(res[1][0][0].f().h/360, 0) if __name__ == "__main__": unittest.main() ================================================ FILE: test/visuals/.gitignore ================================================ _test_pen_to_code_output.py _test_data_src_src.txt ================================================ FILE: test/visuals/test_color_palette.py ================================================ from coldtype.test import * from coldtype.fx.skia import Skfi, mod_pixels, precompose def r10(x): return int(round(x / 150.0)) * 150 def lut(rgba): r, g, b, a = rgba if a < 100: a = 0 if r > 100: return 255, 0, 100, 255 elif g > 100: return 0, 255, 200, 255 elif b > 100: return 0, 155, 255, 255 else: a = 0 return (r, g, b, a) def contrast_cut(mp=127, w=5): ct = bytearray(256) for i in range(256): if i < mp - w: ct[i] = 0 elif i < mp: ct[i] = int((255.0/2)*(1-(mp-i)/w)) elif i == mp: ct[i] = 127 elif i < mp + w: ct[i] = int(127+(255.0/2)*((i-mp)/w)) else: ct[i] = 255 #ct[i] = randint(0, 255) return ct ct = bytearray(256) for i in range(256): x = (i - 96) * 255 // 1 ct[i] = min(255, max(0, x)) @animation(timeline=Timeline(120)) def restricted_colors(f): c = (StSt("COLD", co, 500, wdth=0.5, ro=1).align(f.a.r)) cap_c = (c .copy() .f(0) .attr(skp=dict( Shader=skia.PerlinNoiseShader.MakeFractalNoise(0.01, 0.01, 2, 5))) .ch(precompose(f.a.r)) .attr(skp=dict( ImageFilter=skia.BlurImageFilter.Make(5, 5), ColorFilter=Skfi.as_filter( contrast_cut(f.e("l", 0, r=(50, 190)), 10) , a=0, r=1, g=1, b=1), )) .ch(mod_pixels(f.a.r, 0.1, lambda c: [r10(x) for x in c])) ) #print(cap_c + P(f.a.r)) #return None return cap_c + P(f.a.r).difference(c).f(0) ================================================ FILE: test/visuals/test_gs.py ================================================ from coldtype.test import * @renderable((500, 500)) def test2_(r): return (P() .declare(r:=r.inset(50), cf:=65) .m(r.pw).l(r.ps).l(r.pn) .bxc(r.pe, r.ps, 45) .bxc(r.pn, "sw", cf) .bxc(r.ps.o(-130, 0), r.pe, cf+10) .f(hsl(0.9,l=0.8)).s(0).sw(4) .declare(a:=Line(r.pnw, r.pse)) .line(a) .oval((r.ecy & a).o(100, 100))) @renderable((500, 500)) def test3_(r): return (P() .declare(r:=r.inset(180), c:=335) .m(r.pw).bxc(r.pe, r.pn, c) .bxc(r.pw, r.ps, c).cp() .f(hsl(0.7, l=0.9)).s(0).sw(4) .rotate(90)) @renderable((500, 500)) def test4_(r): return (P() .declare(r:=r.inset(150), x:=r.ee, y:=r.ee.o(50, 0)) .line(x).line(y).oval((r.en & y).o(0, 50)) .declare(r:=r.inset(-20)) .m(r.pnw).bxc(r.ps, "SW,NE", 175).cp() #.gs("(r:=$rI-20)↖ r↓|↙↗|175") .f(None).s(0).sw(4)) ================================================ FILE: test/visuals/test_image_font.py ================================================ from coldtype import * from coldtype.img.skiaimage import SkiaImage from coldtype.fx.skia import skia, precompose lh = Font.Find("liebe") ttfont = lh.font.ttFont sbix = ttfont["sbix"] print(sbix.strikes.keys()) #max_ppem = min(sbix.strikes.keys()) max_ppem = 512 strike = sbix.strikes[max_ppem] glyphs = {} for bitmap in strike.glyphs.values(): if bitmap.graphicType == "png ": glyphs[bitmap.glyphName] = bitmap @renderable(bg=0) def test_leibeheide(r): txt = (StSt("Okey", lh, int(512), metrics="a") .align(r, ty=1) .f(0) .s(0) .sw(117) .mapv(lambda p: p.s(hsl(random())))) #print(txt[0]._val.value) #return P(txt[0]) + P(txt[0].ambit()).fssw(-1, 0, 1) imgs = P() for pen in txt: g = glyphs[pen.glyphName] if g.originOffsetX != 0 or g.originOffsetY != 0: raise Exception("non-zero origin") print(g.glyphName) img = skia.Image.MakeFromEncoded(g.imageData) di = SkiaImage(img) #di.write("test.png") imgs.append(di.translate(pen.ambit(tx=1).x, pen.ambit(ty=1).y)) return P( #txt.frameSet(), txt, #DP(txt[0].ambit(tx=1, ty=1)).f(0, 0.1), #DP(imgs[0].bounds()).f(hsl(0.3, a=0.3)), imgs.ch(precompose(r)).blendmode(BlendMode.Cycle(36)) ) #return txt[0].align(r).skeleton() return txt.translate(0, 660).f(0)#.align(r) print(txt.ambit(), DP(txt.ambit()).align(r).ambit()) return DP(txt.ambit()) ================================================ FILE: test/visuals/test_midi_ctrl.py ================================================ from coldtype import * from coldtype.midi.controllers import LaunchControlXL @animation((1080, 1080), bg=1, rstate=1) def render(f, rstate): nxl = LaunchControlXL(rstate.midi) return P( (P(f.a.r).f(Gradient.V(f.a.r, hsl(nxl(20, 0.25), a=0.5), hsl(nxl(10, 0.45), a=0.5)))), (StSt("MIDI", Font.MutatorSans() , fontSize=20+nxl(12, 0.25)*500 , wdth=nxl(11, 0.25) , wght=nxl(21) , tu=-250+nxl(22)*500 , r=1 , ro=1) .align(f.a.r, tv=1) .f(1))) ================================================ FILE: test/visuals/test_reader_mod.py ================================================ from coldtype.test import * @test((1000, 350)) def test_xstretch(r): st = Style.StretchX(20, debug=1, A=(200, 230), B=(1500, 190), C=(200, 290)) style = Style(mutator, 500, mods=st, wght=0.25) return (StyledString("ABC", style) .pen() .align(r) .scale(0.5) .f(hsl(0.2, a=0.1)) .s(hsl(0.5)) .sw(2)) @test((1000, 350)) def test_xstretch_slnt(r): st = Style.StretchX(20, debug=1, L=(500, (400, 750/2), -14), O=(1000, (385, 750/2), -14)) return (StSt("LOL", co, 500, mods=st) .align(r) .scale(0.5) .fssw(hsl(0.2, a=0.1), hsl(0.5), 2)) @test((1000, 400)) def test_ystretch(r): st = Style.StretchY(20, debug=1, E=(500, 258)) style = Style(mutator, 300, mods=st, wght=0.5) return (StyledString("TYPE", style) .pen() .align(r, tx=1, ty=1) .f(hsl(0.2, a=0.1)) .s(hsl(0.5)) .sw(2)) @test((1000, 400)) def test_ystretch_slnt(r): st = Style.StretchY(20, debug=1, E=(500, (258, 750/2), 35)) style = Style(co, 300, mods=st, wght=0.5) return (StyledString("TYPE", style) .pen() .align(r, tx=1, ty=1) .f(hsl(0.2, a=0.1)) .s(hsl(0.5)) .sw(2) ) ================================================ FILE: tests/_img_only.py ================================================ from coldtype import * @animation((540, 540), bg=None) def scratch(f): res = (StSt("ASDF", Font.MuSan(), 150, wght=f.e("eeio", 1), wdth=0) .align(f.a.r) .f(0) .insert(0, lambda p: P(p.ambit(ty=1).inset(-20)) .fssw(-1, 0, 1) .attr(dash=[5, 2]))) return res ================================================ FILE: tests/test_color.py ================================================ from coldtype.test import * @test() def test_interp(r): a = hsl(0.1).hsl_interp(0, hsl(0.9)) assert a.hp == pytest.approx(0.1) b = hsl(0.1).hsl_interp(1, hsl(0.9)) assert b.hp == pytest.approx(0.9) c = hsl(0.2).hsl_interp(0.5, hsl(0.6)) assert c.hp == pytest.approx(0.4) m = Scaffold(r).cssgrid("a a a", "a", "a b c") return (P( P().rect(m["a"]).f(a), P().oval(m["b"]).f(b), P(m["c"]).f(c))) @test() def test_adjust(r): m = Scaffold(r).numeric_grid(10, 1) return (P().enumerate(m, lambda x: P() .roundedRect(x.el.r, 0.5, scale=False) # 1 for l will erase hue information # a current limitation of the Color class # which stores r,g,b channels .f(hsl(0.6, 0.6, 0.99) .adj(-x.e)))) @test() def test_theme_backfill(r): p1 = P().rect(r.take(0.5, "N").inset(20)).fssw(Theme(hsl(0), alt=hsl(0.6)), -1, 1) p2 = P().rect(r.take(0.5, "S").inset(20)).fssw(-1, Theme(hsl(0), alt=hsl(0.6)), 3) assert p2.styles()["alt"]["stroke"]["color"] == hsl(0.6) assert p2.styles()["alt"]["fill"].a == 0 return p1, p2 ================================================ FILE: tests/test_drawbot.py ================================================ from coldtype.test import * from coldtype.osutil import on_mac not_mac = not on_mac() try: from coldtype.drawbot import * except ModuleNotFoundError: if not_mac: print("Skipping drawBot test (mac-only)") else: print("Must install drawBot") if on_mac(): @drawbot_renderable((800, 100), bg=1, render_bg=0) def test_setup(r): txt = StSt("ASDF", Font.RecMono(), 100).align(r).ch(dbdraw) assert txt.depth() == 1 assert txt.ambit().round() == Rect(278, 15, 240, 70) return txt @drawbot_renderable((800, 200), bg=1, render_bg=0) def test_gs_pen(r): rr = Rect(0, 0, 100, 100) dp = (P() .declare(c:=75) .m(rr.pne).bxc(rr.ps, "se", c) .bxc(rr.pnw, "sw", c).cp() .align(r) .scale(1.2) .f(hsl(0.3, a=0.1)) .s(hsl(0.9)) .sw(5) .ch(dbdraw)) assert len(dp.v.value) == 4 assert type(dp) == P @drawbot_renderable((800, 300), bg=1, render_bg=0) def test_distribute_on_path(r): script = Font.RecMono() s = (StSt("Hello", script, 100) .f(hsl(0.7, s=1)) .align(r) .chain(dbdraw)) with db.savedState(): db.fill(None) db.stroke(0) db.strokeWidth(3) db.rect(*s.ambit()) circle = P().oval(r.inset(100, 20)).rotate(0) s2 = (s.copy() .zero() .distribute_on_path(circle) .chain(dbdraw)) s2a = (P(s2.ambit(tx=1, ty=1)).fssw(-1, hsl(0.9, a=0.3), 10) | dbdraw) with db.savedState(): db.fill(None) db.stroke(0) db.strokeWidth(2) db.oval(*circle.ambit()) assert s.f() == s2.f() assert s2a.ambit(tx=1, ty=1).round() == Rect(403, 19, 265, 114) ================================================ FILE: tests/test_fonts.py ================================================ from coldtype.test import * from coldtype.text.font import FontmakeCache @test() def test_instances(r): font = Font.MutatorSans() instances = font.instances(scaled=True) assert len(instances) == 6 out = StyledString("HIHI", Style(font, 100, instance="BoldCondensed")) assert out.variations["wdth"] == instances["BoldCondensed"]["wdth"]*1000 assert out.variations["wght"] == instances["BoldCondensed"]["wght"]*1000 return out.pens().align(r).f(0) @test() def test_fontmake(r): out = P() src = "assets/ColdtypeObviously_CompressedBlackItalic.ufo" font = Font.Fontmake(src) out.append(StSt("COLD", font, 100)) assert list(FontmakeCache.keys())[0] == Path(src) assert isinstance(font, Font) src = "assets/ColdtypeObviously.designspace" font = Font.Fontmake(src) out.append(StSt("TYPE", font, 100)) assert Path(font.path).suffix == ".ttf" assert list(FontmakeCache.keys())[1] == Path(src) assert isinstance(font, Font) assert len(out) == 2 assert len(out[0]) == 4 assert len(out[1]) == 4 assert out[0][0].data("glyphName") == "C" assert out[-1][-1].data("glyphName") == "E" return out.stack(10).align(r) ================================================ FILE: tests/test_fx.py ================================================ from coldtype.test import * from coldtype.fx.xray import * r = Rect(1000, 1000) @test(250) def test_lookup(_r): out = P() t = StSt("A", Font.MutatorSans(), 1000).pen().fssw(-1, 0, 2) lk = skeletonLookup(t) t.attach(out) assert len(lk["moveTo"]) == 4 assert len(lk["lineTo"]) == 16 assert len(lk["curveOn"]) == 0 assert len(lk["qCurveOn"]) == 0 t = StSt("B", Font.MutatorSans(), 1000).pen().fssw(-1, 0, 2) lk = skeletonLookup(t) t.attach(out) assert len(lk["moveTo"]) == 2 assert len(lk["lineTo"]) == 14 assert len(lk["curveOn"]) == 0 assert len(lk["qCurveOn"]) == 23 t = StSt("C", Font.MutatorSans(), 1000, wght=1).pen().fssw(-1, 0, 2) lk = skeletonLookup(t) t.copy().layer(None, skeleton()).attach(out) assert len(lk["moveTo"]) == 1 assert len(lk["lineTo"]) == 6 assert len(lk["curveOn"]) == 0 assert len(lk["qCurveOn"]) == 24 return out.distribute().scale(0.25).align(_r) ================================================ FILE: tests/test_glyphwise.py ================================================ from coldtype.test import * def _test_glyph_kerning(font_path, kern): txt = "AVAMANAV" ss = StSt(txt, font_path, 100, wdth=0, kern=kern) gw = Glyphwise(txt, lambda g: Style(font_path, 100, wdth=0, kern=kern, ro=1)) gwo = Glyphwise(txt, lambda g: Style(font_path, 100, wdth=0, kern=(not kern), ro=1)) assert len(ss) == len(txt) assert ss[0].glyphName == "A" assert ss[-1].glyphName == "V" assert len(gw) == len(txt) assert ss[0].glyphName == "A" assert ss[-1].glyphName == "V" assert ss.ambit() == gw.ambit() assert ss.ambit() != gwo.ambit() return ss, gw @test(150) def test_format_equality(r): fnt = "OhnoFatfaceVariable.ttf" return (P( *_test_glyph_kerning(fnt, False), *_test_glyph_kerning(fnt, True)) .stack() .scale(0.5) .align(r)) @test(100) def test_ligature(r): clarette = Font.Find("ClaretteGX.ttf") gl = (Glyphwise(["fi", "j", "o", "ff"], lambda g: Style(clarette, 200, wdth=g.i/3)) .align(r)) assert len(gl) == 4 assert gl[0].glyphName == "f_i" assert gl[-1].glyphName == "f_f" #return gl.scale(0.5) gl2 = (Glyphwise2("fijoff", lambda g: Style(clarette, 200, wdth=g.i/3)) .align(r)) assert len(gl2) == 4 assert gl2[0].glyphName == "f_i" assert gl2[-1].glyphName == "f_f" gl3 = (Glyphwise2("fijoff", lambda g: Style(clarette, 200, wdth=g.i/3) , multiline=1) .align(r)) assert len(gl3) == 1 assert len(gl3[0]) == 4 return gl2.scale(0.25) @test(160) def test_variable_args(r): fnt = Font.Find("OhnoFatfaceV") out = P() (Glyphwise("FATFACE 1 ARG", lambda g: Style(fnt, 100, wdth=g.e)) .attach(out)) es = [] def print_e(g): es.append(g.e) return Style(fnt, 100, opsz=g.e, wdth=1-g.e) (Glyphwise("FATFACE 2 ARG", print_e) .attach(out)) assert es[0] == 0 assert es[1] == pytest.approx(0.083333333333) assert es[-1] == 1 return out.stack(10).align(r) @test() def test_newline(r): _r = Rect(1080, 300) fnt = Font.Find("OhnoFatfaceV") gs = (Glyphwise("FATFACE\nFACEFAT", lambda g: Style(fnt, g.i*10+50, wdth=g.e)) .align(_r)) assert gs[0][0].ambit().xy() == [305.21250000000003, 172.05] assert gs[1][-1].ambit().xy() == [656.3775, 58.650000000000006] assert gs[0][-1].glyphName == "E" assert gs[1][-1].glyphName == "T" return gs.align(r) @test(150) def test_newline_onechar(r): fnt = Font.MutatorSans() _r = Rect(1080, 300) gs = (Glyphwise("T\nYPE", lambda g: Style(fnt, 150-g.i*20, wdth=1-g.e)) .xalign(_r) .lead(20) .align(_r) ) assert gs[0][0].ambit().xy() == pytest.approx([454.5, 153.0]) assert gs[1][-1].ambit().xy() == [629.56, 42.0] assert gs[0][0].glyphName == gs[0][-1].glyphName assert gs[0][0].glyphName == "T" assert gs[1][-2].glyphName == "P" return gs.scale(0.5).align(r) @test() def test_kp(r): fnt = Font.Find("OhnoFatfaceV") _r = Rect(1080, 300) gs_no_kp = (Glyphwise("FATFACE", lambda g: Style(fnt, 250, wdth=1-g.e, ro=1)) .align(_r) .fssw(-1, 0, 1)) gs_kp = (Glyphwise("FATFACE", lambda g: Style(fnt, 250, wdth=1-g.e, kp={"A/T":-250}, ro=1)) .align(_r) .fssw(-1, 0, 1)) kp_sw = gs_kp[2].ambit().psw no_kp_sw = gs_no_kp[2].ambit().psw assert kp_sw != no_kp_sw assert kp_sw.y == no_kp_sw.y return (P(gs_kp, gs_no_kp) .stack(10) .align(r) .scale(0.5)) @test() def test_tu(r): fnt = Font.Find("OhnoFatfaceV") _r = Rect(1080, 300) gs_no_tu = (Glyphwise("FATFACE", lambda g: Style(fnt, 250, wdth=1-g.e, ro=1)) .align(_r) .fssw(-1, 0, 1)) gs_tu = (Glyphwise("FATFACE", lambda g: Style(fnt, 250, wdth=1-g.e, tu=-50, ro=1)) .align(_r) .fssw(-1, 0, 1)) assert gs_tu.ambit().w < gs_no_tu.ambit().w return P(gs_no_tu, gs_tu).stack(10).align(r).scale(0.5) @test(80) def test_multistyle(r): def styler1(g): return Style(Font.MutatorSans(), 100, wdth=0) def styler2(g): return [ Style(Font.MutatorSans(), 100, wdth=0), Style(Font.MutatorSans(), 100, wdth=g.e), ] g1 = (Glyphwise("ASDF", styler1) .fssw(-1, 0, 1)) g2 = (Glyphwise("ASDF", styler2) .fssw(-1, 0, 1)) assert g1.ambit().w == g2.ambit().w assert g1.ambit(tx=1).w != g2.ambit(tx=1).w assert g1[-1].glyphName == "F" assert g2[-1].glyphName == "F" assert g2[-1].data("frame").w == g1[-1].data("frame").w assert g1[-1].ambit(tx=1).w == 28.0 assert g2[-1].ambit(tx=1).w == 86.0 return P(g1, g2).distribute().track(100).align(r).scale(0.5) @test(150) def test_multiline(r): def styler(g): return Style(Font.MutatorSans(), 120, meta=dict(idx=g.i)) gw = (Glyphwise("AB\nCD\nEF", styler)) assert len(gw) == 3 gw.collapse() assert len(gw) == 6 for idx, g in enumerate(gw): assert g.data("idx") == idx gw2 = (Glyphwise("ABC", styler, multiline=1)) assert len(gw2) == 1 gw2.collapse() assert len(gw2) == 3 for idx, g in enumerate(gw2): assert g.data("idx") == idx gw3 = (Glyphwise("ABC", styler, multiline=0)) assert len(gw3) == 3 gw3.collapse() assert len(gw3) == 3 for idx, g in enumerate(gw3): assert g.data("idx") == idx return (P(gw, gw2, gw3) .distribute() .track(20) .align(r) .scale(0.5)) @test(10) def test_no_reverse(r): def styler(g): return Style(Font.MutatorSans(), 120, r=1) with pytest.raises(Exception) as context: (Glyphwise("AB\nCD\nEF", styler)) assert "r=1 not possible" in str(context.value) return None @test(100) def test_multistyle_stst(r): stst = (StSt("COLD\nCOLD", [ Style(Font.MuSan(), 30, wght=1), Style(Font.ColdObvi(), 40, wght=1)]) .xalign(r) .align(r)) assert stst[0][0].glyphName == "C" assert stst[-1][-1].glyphName == "D" print(len(stst[0][0]._val.value)) assert len(stst[0][0]._val.value) == 32 assert len(stst[1][0]._val.value) == 32 return stst ================================================ FILE: tests/test_i18n.py ================================================ from coldtype.test import * import unicodedata co = Font.ColdObvi() mutator = Font.MutatorSans() zh = Font.Cacheable("assets/Noto-unhinted/NotoSansCJKsc-Black.otf") latin_font = Font("assets/Noto-unhinted/NotoSans-Black.ttf") arabic_font = Font.Find("GretaArabicCompressedAR-Heavy.otf") ar_light = Font.Find("GretaArabicCondensedAR-Light.otf") hebrew_font = Font.Find(r"GretaSansCondensedH\+L\-Medium\.otf") fnt_en = Font.Cacheable("assets/SourceSerif4-VariableFont_opsz,wght.ttf") r = Rect(1000, 500) def gn_to_c(gn): return chr(int(gn.replace("uni", ""), 16)) def gn_to_uniname(gn): return unicodedata.name(gn_to_c(gn)) @test() def test_mixed_lang_slug(_r): out = P() obv = Style(co, 300, wdth=1, wght=0, lang="en") style = Style(zh, 300, lang="zh") dps = (Slug("CO同步", style, obv) .fit(r.w-100) .pens() .align(r, tx=1)) assert dps[-1].ambit().round() == Rect([657,138,300,220]) assert dps[0].glyphName == "C" assert dps[1].glyphName == "O" assert dps[2].glyphName == "cid12378" assert dps[3].glyphName == "cid23039" assert dps.depth() == 1 dps.attach(out) dps = (Slug("CO同步", style, obv) .fit(r.w-100) .pens(flat=False) .align(r, tx=1)) assert dps[0].data("lang") == "en" assert dps[0][0].glyphName == "C" assert dps[0][1].glyphName == "O" assert dps[1].data("lang") == "zh" assert dps[1][0].glyphName == "cid12378" assert dps[1][1].glyphName == "cid23039" assert dps.depth() == 2 dps.attach(out) return out.stack(100).align(_r).scale(0.25) @test(100) def test_mixed_lang_stst(_r): out = P() dps = (StSt("CO同步TY", zh, 300, lang="zh", fallback=Style(co, 300, wdth=1, wght=0, lang="en"), fit=r.w-100) .align(r, tx=1)) dps[1].translate(10, 0) dps.attach(out) assert dps[1][-1].ambit().w == 300 assert dps[0].data("lang") == "en" assert dps[0][0].glyphName == "C" assert dps[0][1].glyphName == "O" assert dps[1].data("lang") == "zh" assert dps[1][0].glyphName == "cid12378" assert dps[1][1].glyphName == "cid23039" assert dps[2].data("lang") == "en" assert dps[2][0].glyphName == "T" assert dps[2][1].glyphName == "Y" return out.align(_r).scale(0.25) @test() def test_rtl_multiline_stst(_r): out = P() txt = 'Limmmm/Satلل\nوصل الإستيرِو' arabic = Style(ar_light, 150, lang="ar", bs=-1, fallback=Style(latin_font, 100, fill=("hr", 0.5, 0.5))) dps = StSt(txt, arabic, leading=30).xalign(r).align(r) dps.attach(out) lgn = dps[-1][-1][-1].glyphName lc = gn_to_c(lgn) assert unicodedata.name(lc) == "ARABIC LETTER WAW" assert dps[-1][-1][-1].ambit().round() == Rect([707,148,42,87]) assert dps[0][0][0].glyphName == "L" assert dps[0][1][-1].glyphName == "uniFEDF" return out.align(_r).scale(0.65) @test((100)) def test_hebrew(_r): out = P() hebrew = Style(hebrew_font, 130) slug = Slug('קומפרסיה ועוד', hebrew) dps = slug.pens().align(r, tx=1).attach(out) assert gn_to_c(dps[-1].glyphName) == "ק" assert dps[-1].ambit().round() == Rect([719,212,52,76]) return out.align(_r).scale(0.75) @test() def test_multidir_seg_string(_r): txt = "+بوابة" latin = Style(latin_font, 130, fill=("hr", 0.5, 0.5)) arabic = Style(arabic_font, 150, lang="ar", bs=-1) seg = SegmentedString(txt, dict(Arab=arabic, Latn=latin)).pens() slug = Slug(txt, arabic, latin).pens() dps = P([ seg.align(r, tx=1).translate(0, 100), slug.align(r, tx=1).translate(0, -100)]) assert dps[0][-1].glyphName == "plus" assert gn_to_uniname(dps[0][0].glyphName) == "ARABIC LETTER TEH MARBUTA FINAL FORM" assert dps[1][0].glyphName == "plus" assert gn_to_uniname(dps[1][1].glyphName) == "ARABIC LETTER TEH MARBUTA FINAL FORM" return dps.align(_r).scale(0.5) @test(150) def test_combine_slugs(_r): s1 = Slug("YO", Style(co, 300, wdth=1)).pens() line = P().rect(Rect(100, 20)) s2 = Slug("OY", Style(co, 300, wdth=0)).pens() shape = P().oval(Rect(100, 100)) dps = P([s1, line, s2, shape]).distribute().align(r, tx=1) assert dps.ambit().round() == Rect([127,138,737,225]) assert dps[1].ambit().round() == Rect([540,138,100,20]) return dps.align(_r).scale(0.5) @test(120) def test_language_specific_forms(_r): out = P() txt = StSt("ríjks", fnt_en, 350, lang="NLD").attach(out) assert txt[1].glyphName == "iacute" assert txt[2].glyphName == "uni0237" txt = StSt("ríjks", fnt_en, 350).attach(out) assert txt[1].glyphName == "iacute" assert txt[2].glyphName == "j" return out.distribute().track(100).align(_r).scale(0.25) @test(120) def test_korean_fallback(_r): txt = Slug("사이드체인 HPF", Style(zh, 72), Style(fnt_en, 72, fill=hsl(0.3)), print_segments=True).pens().align(_r) assert txt[0].glyphName == "cid52858" assert txt[-1].glyphName == "F" return txt @test(120) def test_language_specific_ufo(_r): hershey_gothic = Font.Find("Hershey-TriplexGothicGerman.ufo") txt = StSt("Grieß".upper(), hershey_gothic, 200, tu=-100) assert len(txt) == 6 assert txt[-1].glyphName == "S" assert txt[-2].glyphName == "S" return txt.outline(2).align(_r).scale(0.5) ================================================ FILE: tests/test_pens.py ================================================ from coldtype.test import * co = Font.Cacheable("assets/ColdtypeObviously-VF.ttf") mutator = Font.Cacheable("assets/MutatorSans.ttf") @test() def test_scaleToRect(_r): r = Rect(1000, 500) dps = P([ (StSt("SPACEFILLING", mutator, 50) .align(r) .f(hsl(0.8)) .scaleToRect(r.inset(100, 100), False)), (StSt("SPACEFILLING", mutator, 50) .align(r) .f(hsl(0.5)) .scaleToWidth(r.w-20)), (StSt("SPACEFILLING", mutator, 50) .align(r) .f(hsl(0.3)) .scaleToHeight(r.h-50))]) assert dps[0].ambit(tx=1).w == pytest.approx(r.inset(100).w) assert dps[0].ambit(ty=1).h == pytest.approx(r.inset(100).h) assert dps[1].ambit(tx=1).w == pytest.approx(r.w-20) assert dps[2].ambit(ty=1).h == r.h-50 return dps.align(_r).scale(0.5) @test(10) def test_underscore_method(r): p1 = P(r.take(0.5, "E")) p1_a1 = p1.ambit() p1.___align(r) # this should do nothing p1_a2 = p1.ambit() p1.align(r) p1_a3 = p1.ambit() assert p1_a1 == p1_a2 assert p1_a1 != p1_a3 assert p1_a2 != p1_a3 return p1 @test(100) def test_distribute_and_track(_r): dps = P() rnd = Random(0) r = Rect(1000, 500) for _ in range(0, 11): dps += (P() .rect(Rect(100, 100)) .f(hsl(rnd.random(), s=0.6)) .rotate(rnd.randint(-45, 45))) dps = (dps .distribute() .track(-50) .reverse() .understroke(s=0.2) .align(r) ) assert len(dps) == 11 assert dps.ambit(tx=1).round().w == 830 return dps.align(_r).scale(0.5) @test() def test_track_to_rect(_r): r = Rect(1000, 500) text = (StSt("COLD", co, 300, wdth=0, r=1) .align(r) .track_to_rect(r.inset(50, 0), r=1)) assert text[0].glyphName == "D" assert text[-1].ambit().round().x == 50 assert text[0].ambit().round().x == 883 return text.align(_r).scaleToRect(_r.inset(10)) @test() def test_distribute_oval(_r): r = Rect(1000, 500) txt = (StSt("COLDTYPE "*7, co, 64, tu=-50, r=1, ro=1, strip=True) .distribute_on_path(P() .oval(r.inset(50)) .reverse() .repeat()) .fssw(hsl(0.9), 0, 3, 1)) assert len(txt) == 62 x, y = txt[-1].ambit().xy() assert round(x) == 500 assert round(y) == 50 txt.unframe().align(_r).scale(0.35, point=_r.pc) return (P( P(txt.ambit()).f(hsl(0.3, 1)), txt)) @test() def test_distribute_path_lines(_r): r = Rect(1080, 1080).inset(200) p = (P().m(r.psw).l(r.pn).l(r.pse).ep()) lockup = (StSt("COLDTYPE", Font.MutatorSans(), 220, wght=.4, wdth=0.5) .distribute_on_path(p) .align(r, ty=1, tx=1) .f(0)) x, y = lockup[3].ambit().xy() assert lockup[3].glyphName == "D" assert round(x) == 454 assert round(y) == 690 x, y = lockup[-1].ambit().xy() assert lockup[-1].glyphName == "E" assert round(x) == 792 assert round(y) == 312 return P(p, lockup).align(_r).scale(0.25) @test((800, 100)) def test_stack(_r): sr = Rect(50, 50) res = (P( P().oval(sr).f(hsl(0.5)).tag("A"), P().oval(sr).f(hsl(0.7)).tag("B"), P().oval(sr).f(hsl(0.9)).tag("C")) .stack(10)) assert res.find_("C").ambit().y == 0 assert res.find_("B").ambit().y == 60 assert res.find_("A").ambit().y == 120 return res.align(_r).scale(0.5) @test((800, 100)) def test_stack_and_lead(_r): sr = Rect(50, 50) res = (P( P().oval(sr).f(hsl(0.5)).tag("A"), P().oval(sr).f(hsl(0.7)).tag("B"), P().oval(sr).f(hsl(0.9)).tag("C")) .stack(10) .lead(10)) assert res.find_("C").ambit().y == 0 assert res.find_("B").ambit().y == 70 assert res.find_("A").ambit().y == 140 return res.align(_r).scale(0.5) @test((800, 150)) def test_projection(r): return (P().rect((300, 300)) .difference(P().rect((-150, -150, 300, 300))) .layer( λ.castshadow(-35, 200).f(hsl(0.1, 1)), λ.project(-35, 200).f(hsl(0.9, 0.8)), λ.f(hsl(0.3, 0.6))) .align(r) .scale(0.25)) @test((800, 150)) def test_plural_boolean(r): r = r.square() res = (P( P(r), P().oval(r.inset(10)), P().oval(r.inset(20))) .intersection() .f(0)) assert res.ambit() == r.inset(20) return res @test((800, 150), solo=0) def test_to_code(r): res = P(r.square().inset(5)).f(hsl(0.3)).data(hello="world") encoded = res.to_code() res2 = eval(encoded) assert res2.data("hello") == "world" assert res.data("hello") == res2.data("hello") assert res.f() == res2.f() res3 = P(res, res2).distribute().track(2).align(r) res4 = eval(res3.to_code()) assert len(res3) == len(res4) assert res3.tree() == res4.tree() assert res3[0].f() == res4[0].f() res4.f(hsl(0.9)) assert res3.f() != res4.f() assert res3[0].f() != res4[0].f() return P(res3, res4).distribute().track(10).align(r) @test((800, 150)) def test_gridlayer(r): res = (P().rect(Rect(30, 30)) .gridlayer(3, 3, 10, 10) .align(r)) res2 = (P().rect(Rect(30, 30)) .layer(3) .spread(10) .layer(3) .stack(10) .align(r)) assert res.ambit() == res2.ambit() assert len(res) == 3 assert len(res[0]) == 3 assert len(res[0][0]) == 0 assert len(res[0][0]._val.value) == 5 res.mapvch(lambda b, p: p.f(hsl(0.65 if b else 0.85))) assert res[0][0].f().h/360 == pytest.approx(0.85) assert res[0][1].f().h/360 == pytest.approx(0.65) res.mapvrc(lambda r, c, p: p.rotate(45 if r == 1 and c == 1 else 0)) assert res[0][0].ambit().w == res[0][1].ambit().w assert res[1][0].ambit().w != res[1][1].ambit().w # the rotated one has a bigger ambit assert res[2][0].ambit().w == res[2][1].ambit().w return res @test((800, 200)) def test_dash_line(r): return (P().line([(_r:=r.inset(30)).psw, _r.pne]) .fssw(-1,0,6) .dash([15, 10.0], -9)) ================================================ FILE: tests/test_reader.py ================================================ from coldtype.test import * from coldtype.osutil import on_mac, on_windows co = Font.Cacheable("assets/ColdtypeObviously-VF.ttf") mutator = Font.Cacheable("assets/MutatorSans.ttf") r = Rect(1000, 500) @test(150) def test_fit(_r): inset = 150 ri = r.inset(inset, 0) out = P([ P(ri).f(hsl(0.7, a=0.1)), (StSt("COLD", co, 500, wdth=1, fit=ri.w, annotate=True) .fssw(-1, hsl(0.7), 2) .align(r))]) assert ri.w == round(out[1].ambit().w) assert out[1]._stst.variations["wdth"] == 379 return out.align(_r).scale(0.25) @test(120) def test_style_mod(r): style = Style(co, 250, wdth=1, annotate=True) a = StSt("CLDTP", style) b = StSt("CLDTP", style.mod(wdth=0)) assert a._stst.variations["wdth"] == 1000 assert b._stst.variations["wdth"] == 0 return P(a, b).stack(10).scale(0.25).align(r) @test(150) def test_fit_height(r): style = Style(co, 250, wdth=1, annotate=True) a = StSt("CLDTP", style) b = StSt("CLDTP", style.mod(fitHeight=300)) assert a._stst.fontSize == 250 assert b._stst.fontSize == 400 return P(a, b).stack(10).align(r).scale(0.25) @test() def test_kern_pairs(_r): style = Style(co, 250, wdth=1, annotate=True) a = StSt("CLD", style) b = StSt("CLD", style.mod(kp={"C/L":20, "L/D":100})) assert a[1].ambit().x == 155.75 assert b[1].ambit().x == 155.75 + 20*(250/1000) assert a[2].ambit().x == 273.5 assert b[2].ambit().x == 273.5 + 20*(250/1000) + 100*(250/1000) return P([a, b]).stack(10).fssw(-1, 0, 1).scale(0.5).align(_r) @test(80) def test_normalize(_r): style = Style("asdf", 100) font_path = Path(style.font.path).relative_to(Path(".").absolute()) assert font_path == Path("packages/coldtype-core/src/coldtype/demo/RecMono-CasualItalic.ttf") assert style.font.names()[0] == "Rec Mono Casual Italic" assert style.font.names()[1] == "Rec Mono Casual" if on_mac(): style = Style("Times", 100) assert style.font.path.name == "Times.ttc" elif on_windows(): style = Style("times", 100) assert style.font.path.name == "times.ttf" return StSt("Hello", style).align(_r).f(0) ================================================ FILE: tests/test_rect.py ================================================ from coldtype.test import * @test((250, 250), solo=0) def test_contains(r): r0 = r.take(150, "SW").offset(10) r1 = r.take(50, "NE").offset(-10) r2 = r.take(50, "SW").offset(20) r3 = r.take(50, "S").take(50, "CX") r4 = r.take(50, "W").take(50, "CY") r5 = r0.take(50, "NE") assert r0.contains(r1) == False assert r0.contains(r2) == True assert r0.contains(r3) == False assert r0.contains(r4) == False assert r3.contains(r4) == False assert (r5 in r0) == True return P(P(x) for x in [r0,r1,r2,r3,r4,r5]).fssw(-1, 0, 1) @test((250, 250)) def test_fit(r): r1 = P(r.take(20, "NE")) assert r1.ambit().w == 20 assert r1.ambit().x == 250-20 rs = (r1.layer( 1, lambda p: p.align(r, "E"), lambda p: p.align(r, "SE"), lambda p: p.align(r, "S"), lambda p: p.align(r, "SW"), lambda p: p.align(r, "W"), lambda p: p.align(r, "NW"), lambda p: p.align(r, "N"), )) assert len(rs) == 8 assert rs.ambit().w == 250 assert rs.ambit().h == 250 assert rs[3].ambit().y == 0 assert rs[5].ambit().x == 0 return rs @test((250, 250), solo=0) def test_align_text(r): txt = (StSt("Hello\nWorld\nLonger Line", Font.RecMono(), 32) .xalign(r, "E")) assert txt[0].ambit().y == pytest.approx(64.8) assert txt[1].ambit().y == pytest.approx(32.4) assert txt[2].ambit().y == 0 assert txt[0].ambit().x == pytest.approx(155.98, rel=1e-4) assert txt[1].ambit().x == pytest.approx(154.84, rel=1e-4) assert txt[2].ambit().x == pytest.approx(40.47, rel=1e-4) return txt @test((250, 250), solo=0) def test_aspects(r:Rect): ri = r.inset(20) r45 = ri.fit_aspect(4, 5) assert r45.w == 168 assert r45.h == ri.h r54 = ri.fit_aspect(5, 4) assert r54.w == ri.w assert r54.h == r45.w r169 = ri.fit_aspect(16, 9) assert r169.w == ri.w assert r169.h == 118.125 return (P( P(r45), P(r54), P(r169), ) .fssw(-1, 0, 1)) ================================================ FILE: tests/test_richtext.py ================================================ from coldtype.test import * from coldtype.text.richtext import RichText f1 = Font.ColdtypeObviously() f2 = Font.MutatorSans() @test() def test_preserve_space(_r): r = Rect(1200, 300) rt = RichText(r, "HELLO[i] COLDTYPE", dict( i=Style(f2, 200, wdth=0, wght=1), default=Style(f1, 200, wdth=0))).align(r) assert rt[0][0].data("txt") == "COLDTYPE" assert rt[0][1].data("txt") == "HELLO " assert rt[0][1][0].glyphName == "space" assert rt[0][1][-1].glyphName == "H" assert rt[0][0][0].glyphName == "E" assert rt[0][0][-1].glyphName == "C" space_width = rt[0][1][0].ambit(tx=0).w assert space_width == 50 assert rt[0][1].ambit(tx=0).w - space_width > rt[0][1].ambit(tx=1).w return rt.align(_r).scale(0.5) @test() def test_ligature(_r): clarette = Font.Find("ClaretteGX.ttf") txt = "fi¬joff≤asdf≥" r = Rect(1080, 300) gl = (RichText(r, txt, dict( default=Style(clarette, 200), asdf=Style(clarette, 200, wdth=1)), tag_delimiters=["≤", "≥"], visible_boundaries=["¶"], invisible_boundaries=["¬"]) .align(r)) assert gl[0][0].data("style_names")[0] == "asdf" assert len(gl[0][1].data("style_names")) == 0 assert gl[0][0][0].glyphName == "f_f" assert gl[0][1][0].glyphName == "f_i" return gl.align(_r).scale(0.5) ================================================ FILE: tests/test_scaffold.py ================================================ from coldtype.test import * from coldtype.runon.runon import RunonSearchException, RunonException @test((500, 250)) def test_auto_recursion(r): l = Scaffold(r) assert l.depth() == 0 assert l.v == l.r assert l.v == l.rect l.divide(0.5, "N") assert l.depth() == 1 assert l.rect.w == 500 assert l.rect.h == 250 assert l[0].rect.w == 500 assert l[0].rect.h == 250/2 assert l[1].rect.h == 250/2 l.divide(200, "W") assert l.depth() == 2 assert l.rect.w == 500 assert l.rect.h == 500/2 assert l[0].rect.w == 500 assert l[0].rect.h == 250/2 assert l[1].rect.h == 250/2 assert l[0][0].rect.w == 200 assert l[1][0].rect.w == 200 l[1][1].subdivide(3, "W") assert l[1][1][1].rect.w == 100 return P(l[0]) @test((500, 250)) def test_duck(r): l = Scaffold(r) l.grid(2, 2, "abcd") assert l.val_present() == False assert l[0].tag() == "a" assert l[-1].tag() == "d" txt = (StSt("ASDF", Font.MutatorSans(), 50)) assert txt.ambit().x == 0 txt1 = txt.copy().align(l["b"]) txt2 = txt.copy().align(l["b"].rect) assert txt.ambit().x == 0 assert txt1.ambit().x == 333.725 assert txt2.ambit().x == 333.725 return txt1, P(l[0]) @test((500, 250)) def test_cssgrid(_r): l = (Scaffold(Rect(_r)) .cssgrid(r"auto 30%", r"50% auto", "x y / z q", x=("200 a", "a a", "a b / a c"), c=("a a", "a a", "g a / i a"), q=("a a", "a a", "q b / c d"))) assert l.depth() == 3 assert l["x"] is not None assert l["x/c"] is not None assert l["x/c/a"] is not None assert l.get("x/c/q") is None with pytest.raises(RunonSearchException) as context: l["x/c/z"] assert l["x/a"] != l["x/c/a"] assert l["x/c/a"] != l["a"] assert l["q"] != l["q/q"] return P(l["x/a"]) + P(l["q"]) @test((500, 500)) def test_cssgrid_regexs(r): # vulf compressor adv (mini) s = Scaffold(r).cssgrid("a a a", "a 100 60", "a || b || c _/ d d d _/ e e e", { r"^[abc]": ("a a", "a 30", "a b _/ c c"), "d": ("a 60", "a a", "a b / c d")}) assert s["a"].r.w == 166 assert s["a/b"].r.w == s["b/b"].r.w - 1 assert s["a/b"].r.w != s["d/b"].r.w assert s["d/b"].r.w == 60 assert s["a"].depth() == 1 assert s["d"].depth() == 1 assert s["e"].depth() == 0 assert s["a/a"].depth() == 0 return P( P(s["a/b"]), P(s["b/b"]), P(s["c/b"]), P(s["d/b"]), P(s["e"])) @test((500, 250)) def test_cssgrid_nested(r): s = Scaffold(r).cssgrid("a a a", "a a a", "a b c / d e f / g h i", { "a": ("a a", "a a", "a b / c d"), "a/b": ("a a a", "a", "a b c"), r"h|i": ("a a", "a", "a b"), }) assert s["a/b/b"].r == Rect(110.00,209.00,28.00,41.00) assert s["h/b+i/a"].r == Rect(249.00,0.00,167.00,84.00) return P( P(s["a/b/b"]).f(0), P(s["h/b+i/a"]).f(hsl(0.65)), ) @test((500, 250)) def test_wildcards(r): s = Scaffold(r).cssgrid("a a", "a a", "a b / c d", { ".*": ("a a", "a a", "a b / c d"), ".*/.*": ("a a", "a", "a b"), }) assert s.depth() == 3 assert len(s) == 4 assert len(s["a"]) == 4 assert len(s["a/a"]) == 2 assert len(s["a/a/a"]) == False assert len(s.match("a")) == 1 assert len(s.match("a/a")) == 1 assert len(s.match("a/a/.*")) == 2 s2 = s.copy().collapse() assert len(s2) == 32 assert len(s2.match("a")) == 16 assert len(s2.match("b")) == 16 return P( P(s["a/a/a"]).f(hsl(0.5)), P(s["c/b/b"]).f(hsl(0.07, 0.7)), P(s["d/d/b"]).f(hsl(0.8)), ) @test((500, 500)) def test_labeled_grid(r): ri = r.inset(20) s = Scaffold.AspectGrid(ri, 4, 5) assert s.r.w < ri.w assert s.r.h == ri.h assert len(s) == 20 assert s[0].tag() == "0|4" assert s[-1].tag() == "3|0" return s.view() @test((500, 500), solo=0) def test_numeric_grid(r): ri = r.inset(20) s = Scaffold(ri).numeric_grid(5, gap=4, annotate_rings=1) assert len(s) == 81 assert len(s.cells()) == 25 assert s["2|2"].data("ring") == 0 assert s["2|2"].data("ring_e") == 0 assert s["3|2"].data("ring") == 1 assert s["4|1"].data("ring") == 2 assert s["4|4"].data("ring_e") == 1 p = (P().enumerate(s.cells(), lambda x: P() .oval(x.el.r) .f(0) .up() .append(StSt(x.el.tag(), Font.JBMono(), 24, wght=1) .f(1) .align(x.el.r)) .tag(x.el.tag()))) assert p.find_("4|4").ambit().pne == ri.pne assert p.find_("0|0").ambit().psw == ri.psw assert p.find_("0|4").ambit().pnw == ri.pnw assert p.find_("4|0").ambit().pse == ri.pse assert s["0|1*3|2"].r == s["0|1+2|2"].r assert s["-1|-1"].r == s["4|4"].r assert s["0|-1*3|-2"].r == s["0|4+2|3"].r return p ================================================ FILE: tests/test_src_macro.py ================================================ from coldtype.test import * co = Font.Cacheable("assets/ColdtypeObviously-VF.ttf") mutator = Font.Cacheable("assets/MutatorSans.ttf") r = Rect(1000, 500) # assert 4j == 0.1016 # see .coldtype.py for magic # @test(150) # def test_src_macro(_r): # return (P( # StSt("4j", Font.RecMono(), fs:=60), # StSt(str(4j), Font.RecMono(), fs)) # .stack(20) # .align(_r) # .f(hsl(0.9))) ================================================ FILE: tests/test_text.py ================================================ from coldtype.test import * tf = Path(__file__).parent def _test_glyph_names(r, font_path): ss = StSt("CDELOPTY", font_path, 300, wdth=0).align(r) assert len(ss) == 8 assert ss[0].glyphName == "C" assert ss[-1].glyphName == "Y" return ss @test((800, 150)) def test_format_equality(r): _r = Rect(800, 300) ttf = _test_glyph_names(_r, Font.ColdtypeObviously()) otf = _test_glyph_names(_r, "assets/ColdtypeObviously_CompressedBlackItalic.otf") ufo = _test_glyph_names(_r, "assets/ColdtypeObviously_CompressedBlackItalic.ufo") ds = _test_glyph_names(_r, "assets/ColdtypeObviously.designspace") # TODO why isn't the ttf version equal to these? assert ufo[0].v.value == ds[0].v.value assert ufo[-1].v.value == ds[-1].v.value return P([ ttf, otf, ufo, ds ]).fssw(-1, hsl(0.5, a=0.1), 3).scale(0.5).align(r) @test((800, 100)) def test_space(r): _r = Rect(2000, 1000) txt = StSt("A B", Font.MutatorSans(), 1000) space = txt[1] assert space.glyphName == "space" assert space.ambit().w == 250 assert txt.ambit().w == 1093 assert space.ambit().x == 400 txt.align(_r, tx=1) assert space.ambit().x == 863.5 txt = StSt("A B", Font.MutatorSans(), 1000, space=500) space = txt[1] assert space.glyphName == "space" assert space.ambit().w == 500 assert txt.ambit().w == 1093+250 assert space.ambit().x == 400 txt.align(_r, tx=1) assert space.ambit().x == 863.5-(250/2) return txt.scale(0.1).align(r) @test((800, 100)) def test_static_fonts(r): _r = Rect(1200, 300) f1 = Font.ColdtypeObviously() co = (StSt("CDELOPTY", f1, 200) .align(_r)) assert co.ambit(tx=0).w == pytest.approx(998.8) assert co.ambit(tx=1).w == pytest.approx(1018.8, 1) assert len(co) == 8 assert f1.font.fontPath.stem == "ColdtypeObviously-VF" return co.align(r).scale(0.5) @test((800, 100)) def test_color_font(r): txt = StSt("C", "PappardelleParty-VF.ttf", 100, palette=2) assert len(txt) == 1 assert len(txt[0]) == 3 assert txt[0][0].glyphName == "C_layer_0" assert txt[0][-1].glyphName == "C_layer_2" assert txt[0][0].f().h == pytest.approx(176.666, 2) txt = StSt("COLDTYPE", "PappardelleParty-VF.ttf", 100) assert txt[0][0].f().h == pytest.approx(196.318, 2) return txt.align(r) @test((800, 150)) def test_narrowing_family(r): _r = Rect(1080, 300) style = Style("Nikolai-Bold.otf", 200, narrower=Style("Nikolai-NarrowBold.otf", 200, narrower=Style("Nikolai-CondBold.otf", 200))) out = P() txt = StSt("Narrowing", style, fit=_r.w) assert int(txt.ambit().w) == 928 out.append(txt.align(r)) txt = StSt("Narrowing", style, fit=_r.w-200) assert int(txt.ambit().w) == 840 out.append(txt.align(r)) txt = StSt("Narrowing", style, fit=_r.w-400) assert int(txt.ambit().w) == 733 out.append(txt.align(r)) txt = StSt("Narrowing", style, fit=_r.w-600) assert int(txt.ambit().w) == 733 out.append(txt.align(r)) return out.stack(10).scale(0.25).align(r) @test((800, 150)) def test_unstripped_text(r): st1 = StSt("HELLO\n", Font.MutatorSans(), 100, strip=True) assert len(st1) == len("HELLO") st2 = StSt("HELLO\n", Font.MutatorSans(), 100) assert len(st2) == 2 st3 = st2.collapse().deblank() assert len(st3) == len(st1) st4 = StSt("\n\nHELLO\n", Font.MutatorSans(), 100, strip=True) assert len(st4) == len("HELLO") st5 = StSt("\n\nHEL\nL\nO\n", Font.MutatorSans(), 100, strip=True) assert len(st5) == 3 return st5.scale(0.5).align(r) @test((800, 10)) def test_zero_font_size(r): s = Style(Font.ColdtypeObviously(), 0) assert s.fontSize == 0 s = Style(Font.ColdtypeObviously(), -1) assert s.fontSize == 0 s = Style(Font.ColdtypeObviously(), -1000) assert s.fontSize == 0 st = StSt("COLD", Font.ColdtypeObviously(), 0) assert len(st) == 4 assert st[0].glyphName == "C" assert st[-1].glyphName == "D" return None @test((800, 120)) def test_strict_multiline_stst(r): out = P() st = (StSt("COLD", Font.ColdtypeObviously(), 100, multiline=1) .attach(out)) assert len(st) == 1 assert len(st[0]) == 4 st = (StSt("COLD\nTYPE", Font.ColdtypeObviously(), 100, multiline=1).attach(out)) assert len(st) == 2 assert len(st[1]) == 4 st = (StSt("COLD\nTYPE", Font.ColdtypeObviously(), 100, multiline=0).attach(out)) assert len(st) == 2 assert len(st[1]) == 4 return (out.distribute().track(10) .map(lambda i,p: p.f(hsl(i*0.2))) .scale(0.5) .align(r)) @test((800, 80)) def test_depth(r): out = P() st = (StSt("These are some words", Font.RecursiveMono())).attach(out) assert st.depth() == 1 st = (StSt("These are some words", Font.RecursiveMono(), multiline=1)).attach(out) assert st.depth() == 2 st = P([(StSt("These are some words", Font.RecursiveMono(), multiline=1))]).attach(out) assert st.depth() == 3 st = (StSt("These are some words\nbut now on multiple lines\nisn't that interesting", Font.RecursiveMono())).attach(out) assert st.depth() == 2 return (out .distribute() .track(10) .align(r) .scale(0.5) .map(lambda i,p: p.f(hsl(i*0.2)))) @test((800, 100), solo=1) def test_word_splitting(r): st = (StSt("These are some words", Font.RecursiveMono())) assert st.depth() == 1 assert st[0].glyphName == 'T' assert len(st) == 20 st = st.wordPens(consolidate=True) assert st.depth() == 1 assert st[0].data("word") == 'T/h.italic/e.italic/s.italic/e.italic' assert len(st) == 4 st = (StSt("These are some words", Font.RecursiveMono(), multiline=1)) assert st.depth() == 2 assert st[0][0].glyphName == 'T' assert len(st[0]) == 20 st = st.wordPens(consolidate=True) assert st.depth() == 2 assert st[0][0].data("word") == 'T/h.italic/e.italic/s.italic/e.italic' assert len(st[0]) == 4 st = (StSt("These are\nsome words", Font.RecursiveMono())) assert st.depth() == 2 assert st[0][0].glyphName == 'T' assert len(st[0]) == 9 st = st.wordPens(consolidate=True) assert st.depth() == 2 assert st[-1][-1].data("word") == 'w.italic/o/r.italic/d.italic/s.italic' assert len(st[0]) == 2 return st.align(r) ================================================ FILE: tests/test_versions.py ================================================ from coldtype.test import * @test((800, 150), rstate=1) def test_versions_sidecar(r, rs): assert rs.renderer.source_reader.config.restart_count == 0 assert __VERSION__["key"] == "test_color" assert len(__VERSIONS__) == 15 # i.e. number of visual tests in this folder ================================================ FILE: tests/test_versions_versions.py ================================================ from coldtype import * VERSIONS = {} for file in sorted(__sibling__(".").glob("*.py")): if not file.stem.startswith("_"): VERSIONS[file.stem] = dict(file=file) ================================================ FILE: upload_docs.sh ================================================ aws s3 --region us-east-1 sync --cache-control no-cache --exclude "*" --include "*.html" docs/notebooks/site s3://coldtype.goodhertz.com aws s3 --region us-east-1 sync docs/notebooks/site s3://coldtype.goodhertz.com