Skip to content

Instantly share code, notes, and snippets.

@mathieureguer
Created February 25, 2022 15:59
Show Gist options
  • Save mathieureguer/b78f115bb9a81faf8ac567560b9ebadc to your computer and use it in GitHub Desktop.
Save mathieureguer/b78f115bb9a81faf8ac567560b9ebadc to your computer and use it in GitHub Desktop.
# this is largely stolen from Antonio Cavedoni's much cooler CAShapeLayer animation demo.
import vanilla
from mojo.events import addObserver, removeObserver
from mojo.UI import splitText
from fontTools.pens.basePen import BasePen
from AppKit import NSView, NSMakeRect, NSColor, CAShapeLayer, NSRect, CAScrollLayer
from Quartz.QuartzCore import kCALayerWidthSizable, kCALayerHeightSizable, CABasicAnimation
from Quartz import CoreGraphics as CG
import time
# ----------------------------------------
SCALE = .04
LINE_LENGTH = 50
WORD_BREAKS = ["space"]
# CONTENT = ['a', 'b', ]
# CONTENT = ['a', 'b', 'c', 'd', 'e', 'space', 'f', 'g', 'h', 'i', 'space', 'j', 'k', 'l', 'm', 'n', 'space', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'space'] * 30
INPUT = "Call me Ishmael.\nSome years ago —never mind how long precisely— having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people’s hats off–then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship."
MARGIN = 20
# ----------------------------------------
class CGPen(BasePen):
def __init__(self, glyphSet, xform=None):
BasePen.__init__(self, glyphSet)
# xform is an optional CG.CGAffineTransform()
self.xform = xform
self.cgpath = CG.CGPathCreateMutable()
def _moveTo(self, pt):
x, y = pt
CG.CGPathMoveToPoint(self.cgpath, self.xform, x, y)
def _lineTo(self, pt):
x, y = pt
CG.CGPathAddLineToPoint(self.cgpath, self.xform, x, y)
def _curveToOne(self, p1, p2, p3):
x1, y1 = p1
x2, y2 = p2
x3, y3 = p3
CG.CGPathAddCurveToPoint(self.cgpath, self.xform, x1, y1, x2, y2, x3, y3)
def _closePath(self):
CG.CGPathCloseSubpath(self.cgpath)
# ----------------------------------------
class LayerWrapper():
cached_paths = {}
def __init__(self, glyph, scale, frame=(0, 0, 0, 0)):
self.glyph = glyph
self.scale = scale
self.layer = CAShapeLayer.alloc().init()
self.set_frame(frame)
self.set_path()
self.layer.setFillColor_(NSColor.blackColor().CGColor())
# self.layer.setBackgroundColor_(NSColor.blueColor().CGColor())
self.layer.setDrawsAsynchronously_(True)
def set_path(self):
self.layer.setPath_(self._get_cached_path())
def set_frame(self, x_y_w_h_tuple):
x, y, w, h = x_y_w_h_tuple
frame = NSRect((x, y), (w, h))
self.layer.setFrame_(frame)
def _get_cached_path(self):
path = self.cached_paths.get(self.glyph, None)
if not path:
path = self._get_glyph_CGPath()
self.cached_paths[self.glyph] = path
return path
def _get_glyph_CGPath(self):
xform = CG.CGAffineTransformMake(
self.scale, 0, 0, self.scale, 0, 0
)
pen = CGPen(self.glyph.font, xform)
self.glyph.draw(pen)
return pen.cgpath
def set_fill_color(self, color):
self.layer.setFillColor_(color)
def remove_from_superlayer(self):
self.layer.removeFromSuperlayer()
@property
def width(self):
return self.glyph.width * self.scale
def __repr__(self):
return(f"LayerWrapper {self.glyph.name}")
class LayerWrapperNewline():
def __init__(self, frame=(0, 0, 0, 0)):
self.layer = CAShapeLayer.alloc().init()
self.glyph=None
self.set_frame(frame)
def set_frame(self, x_y_w_h_tuple):
x, y, w, h = x_y_w_h_tuple
frame = NSRect((x, y), (w, h))
self.layer.setFrame_(frame)
def set_fill_color(self, color):
pass
def remove_from_superlayer(self):
self.layer.removeFromSuperlayer()
@property
def width(self):
return 0
def __repr__(self):
return(f"LayerWrapperNewLine")
class ManyLetterWindow:
def __init__(self):
# f = currentGlyphChanged()
self.font = CurrentFont()
self.cmap = self.font.getCharacterMapping()
self.input = INPUT
self.layer_record = []
self.previous_current_glyph = None
# create a vanilla Window
self.w = vanilla.Window(
(1400, 850),
"loads of glyphs",
minSize=(200, 200),
maxSize=(2000, 2000),
)
self.w.bind("close", self.window_close_callback)
self.w.bind("resize", self.window_resize_callback)
self.text_view = NSView.alloc().init()
self.text_view.setFrame_(((0, 0), (1400, 850)))
self.w.scrollview = vanilla.ScrollView((0, 60, -0, -0),
self.text_view)
# get a reference to the content view
# self.contentView = self.w.getNSWindow().contentView()
# set the content view to layer-backed
self.text_view.setWantsLayer_(True)
# self.contentViewScroll = CAScrollLayer.alloc().initWithFrame(self.w.getNSWindow().contentView().frame())
# self.contentView.layer().addSublayer_(self.contentViewScroll)
current_height = 850 - MARGIN - 1200 * SCALE
current_width = MARGIN
count = 0
# self.w.text_input = vanilla.EditText((20, 20, -200, 22),
# text=INPUT,
# continuous=True,
# callback=self.update_text_view)
self.w.input_button = vanilla.SquareButton((20, 20, 22, 22), "T", callback=self.input_button_callback)
self.w.glyph_count = vanilla.TextBox((-160, 20, -20, 22), "")
# self.pop = vanilla.Popover((180, 180), preferredEdge='top', behavior='semitransient')
self.update_text_view()
self.w.open()
# self.pop.open(parentView=self.w.getNSWindow().contentView())
addObserver(self, "draw_glyphs", "draw")
addObserver(self, "currentGlyphChanged", "currentGlyphChanged")
# helpers
def _layer_all_glyphs(self, glyphs, scale):
# reset sub layers
self._reset_layer_record()
for g in glyphs:
if g == None:
pass
elif g == "\n":
layer = LayerWrapperNewline()
else:
layer = LayerWrapper(g, scale)
self._add_layer(layer)
self._position_layers()
def _reset_layer_record(self):
for l in self.layer_record:
l.remove_from_superlayer()
self.layer_record = []
def _get_glyph_list_from_input(self):
input_ = self.input
glyph_names = splitText(input_, self.cmap)
return [n if n=="\n" else self.font[n] if n in self.font.keys() else None for n in glyph_names ]
def _add_layer(self, layerWrapper):
self.text_view.layer().addSublayer_(layerWrapper.layer)
# self.contentViewScroll.addSublayer_(layerWrapper.layer)
self.layer_record.append(layerWrapper)
def _position_layers(self):
line_height = 1200 * SCALE
scroll_frame = self.w.scrollview.getNSScrollView().frame()
view_width = scroll_frame[1][0]
view_height = scroll_frame[1][1]
slugs = self._split_layers_in_slugs(self.layer_record, view_width - (MARGIN * 2))
required_height = line_height * len(slugs) + MARGIN * 2
target_height = max(view_height, required_height)
self.text_view.setFrame_(NSRect((0, 0), (view_width, target_height)))
current_height = target_height - MARGIN - line_height
current_width = MARGIN
for slug in slugs:
for layer in slug:
layer.set_frame((current_width, current_height, 0, 0))
current_width += layer.width
current_width = MARGIN
current_height -= line_height
def _split_layers_in_slugs(self, layer_list, line_length, dont_break_words=True):
slugs = []
slug = []
word = []
word_length = 0
current_width = 0
for l in layer_list:
# hit a newline
if isinstance(l, LayerWrapperNewline):
word.append(l)
current_width += l.width
slug += word
word = []
word_length = 0
slugs.append(slug)
slug = []
current_width = word_length
# hit a word break, add the word to the slug
elif l.glyph.name in WORD_BREAKS:
word.append(l)
current_width += l.width
slug += word
word = []
word_length = 0
else:
# going over line length, oh no!
if current_width + l.width > line_length:
# the word is longer than a line, there no other choice than to break it
if slug == []:
slugs.append(word)
word = []
word_length = 0
current_width = 0
# add the slug to the slugs record and reset it
else:
slugs.append(slug)
slug = []
current_width = word_length
word.append(l)
current_width += l.width
word_length += l.width
slug += word
slugs.append(slug)
return slugs
# observer event
def draw_glyphs(self, event):
# redraw the necessary layers
glyph = event["glyph"]
# delete the cached path
LayerWrapper.cached_paths[glyph] = None
layers_to_update = [l for l in self.layer_record if l.glyph == glyph]
for layer in layers_to_update:
layer.set_path()
# layer.layer.setFillColor_(NSColor.redColor().CGColor())
def currentGlyphChanged(self, event):
current_glyph = event["glyph"]
if self.previous_current_glyph:
layers = [l for l in self.layer_record if l.glyph == self.previous_current_glyph]
for layer in layers:
layer.set_fill_color(NSColor.blackColor().CGColor())
layers = [l for l in self.layer_record if l.glyph == current_glyph]
for layer in layers:
layer.set_fill_color(NSColor.redColor().CGColor())
self.previous_current_glyph = current_glyph
def window_close_callback(self, sender):
removeObserver(self, "draw")
removeObserver(self, "currentGlyphChanged")
def window_resize_callback(self, sender):
self._position_layers()
def update_text_view(self):
t = time.time()
glyphs = self._get_glyph_list_from_input()
print(f"get glyphs {time.time()-t}")
t = time.time()
self._layer_all_glyphs(glyphs, SCALE)
print(f"get layer {time.time()-t}")
t = time.time()
self.w.glyph_count.set(f"{len(glyphs)} glyphs displayed")
print(f"get count {time.time()-t}")
print("")
def input_button_callback(self, sender):
self.open_input_sheet()
def open_input_sheet(self):
self.input_sheet = vanilla.Sheet((400, 400), self.w, minSize=(200, 200))
self.input_sheet.text = vanilla.TextEditor((0, 0, -0, -62))
self.input_sheet.typeset_button = vanilla.Button((20, -42, -20, 22), "Typeset", callback=self.input_sheet_typeset_callback)
self.input_sheet.text.set(self.input)
self.input_sheet.open()
def input_sheet_typeset_callback(self, sender):
self.input = self.input_sheet.text.get()
self.update_text_view()
self.input_sheet.close()
ManyLetterWindow()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment