Files
endorphin-tessera-gui/examples/illustrated-manuscript.endor
T

298 lines
12 KiB
Plaintext

// All application state, motion, fire, hit reactions and scheduling live here.
// Tessera only supplies generic graphics/text/geometry, input and owned Tasks.
g = ui.graphics
math = g.math
assets = "examples/assets/manuscript/"
font = g.loadFont(assets + "IMFellEnglish-Regular.ttf", "IM FELL English")
story = g.textSlice(g.readText(assets + "story.txt"), 1, 1000000)
bodyText = g.prepareText(story, font, 21, 34, "#2A1A0A", 2)
smallText = g.prepareText(story, font, 16, 26, "#2A1A0A", 2)
hint = g.prepareText("Move the pointer. Hold to breathe fire.", font, 14, 22, "#705B44", 2)
function sprite(image: Object, x: Number, y: Number, scale: Number, angle: Number, ax: Number, ay: Number): Object {
return g.sprite(image, x, y, image.width * scale, image.height * scale, angle, ax, ay, 1)
}
function wrappedAngle(angle: Number): Number {
result = angle
while result > math.pi { result = result - math.pi * 2 }
while result < -math.pi { result = result + math.pi * 2 }
return result
}
class Segment {
x: Number
y: Number
angle: Number
image: Object
}
// Asset orientation is application data: angles are radians, clockwise in
// screen coordinates. Each PNG is calibrated against its own long axis.
class FireArt {
image: Object
axis: Number
}
class Direction { angle: Number }
class Flame {
x: Number
y: Number
vx: Number
vy: Number
size: Number
age: Number
art: FireArt
}
class Dragon {
segments: Object
flames: Object
fireImages: Object
tongue: Object
frontWing: Object
backWing: Object
targetX: Number
targetY: Number
pressed: Number
time: Number
lastMove: Number
seed: Number
function random(self: Dragon): Number {
self.seed = self.seed + 1
value = math.sin(self.seed * 12.9898) * 43758.5453
return value - math.floor(value)
}
function pointer(self: Dragon, event: Event): Unit {
bounds = event.mount.bounds()
scale = math.max(0.2, math.min(1, (bounds.width - 40) / 700))
self.targetX = (event.x - (bounds.width - 700 * scale) / 2) / scale
self.targetY = (event.y - 30) / scale
self.lastMove = math.now()
}
function down(self: Dragon, event: Event): Unit {
self.pointer(event)
self.pressed = 1
}
function up(self: Dragon, event: Event): Unit { self.pressed = 0 }
function leave(self: Dragon, event: Event): Unit {
self.pressed = 0
self.lastMove = -10
}
function step(self: Dragon): Unit {
self.time = math.now()
if self.time - self.lastMove > 2 {
x = math.float(150)
y = math.float(110)
i = 0
while i < self.segments.count {
s = self.segments.at(i)
angle = -i / 19 * math.pi * 0.7
if i > 0 {
x = x - math.cos(angle) * 30
y = y - math.sin(angle) * 30
}
s.x = s.x + (x - s.x) * 0.12
s.y = s.y + (y - s.y) * 0.12
s.angle = s.angle + wrappedAngle(angle - s.angle) * 0.12
i = i + 1
}
} else {
head = self.segments.at(0)
dx = self.targetX - head.x
dy = self.targetY - head.y
distance = math.sqrt(dx * dx + dy * dy)
if distance > 4 {
speed = math.min(distance, math.max(12, distance * 0.15))
head.x = head.x + dx / distance * speed
head.y = head.y + dy / distance * speed
head.angle = math.atan2(dy, dx)
}
i = 1
while i < self.segments.count {
previous = self.segments.at(i - 1)
s = self.segments.at(i)
angle = math.atan2(previous.y - s.y, previous.x - s.x)
s.angle = previous.angle + math.clamp(wrappedAngle(angle - previous.angle), -0.25, 0.25)
s.x = previous.x - math.cos(s.angle) * 30
s.y = previous.y - math.sin(s.angle) * 30
i = i + 1
}
}
i = self.flames.count
while i > 0 {
i = i - 1
p = self.flames.at(i)
p.age = p.age + 1
p.x = p.x + p.vx
p.y = p.y + p.vy
p.vx = p.vx * 0.95
p.vy = p.vy * 0.95 - 0.6
if p.age >= 12 { self.flames.remove(i) }
}
if self.pressed > 0 {
head = self.segments.at(0)
i = 0
while i < 1 {
if self.flames.count < 12 {
angle = head.angle + (self.random() - 0.5) * 0.25
speed = 20 + self.random() * 12
size = 12 + self.random() * 8
art = self.fireImages.at(math.floor(self.random() * self.fireImages.count))
self.flames.append(Flame(head.x + math.cos(head.angle) * 30, head.y + math.sin(head.angle) * 30, math.cos(angle) * speed, math.sin(angle) * speed, size, 0, art))
}
i = i + 1
}
}
}
function artwork(self: Dragon, ox: Number, oy: Number, scale: Number): Object {
commands = ObjectList()
wing = self.segments.at(5)
commands.append(sprite(self.backWing, ox + wing.x * scale, oy + wing.y * scale, scale * 0.24, wing.angle + math.sin(self.time * 3) * 0.4, 1, 1))
i = self.segments.count
while i > 0 {
i = i - 1
s = self.segments.at(i)
ax = math.float(0.5)
if i == 0 {
ax = 0.45
commands.append(sprite(self.tongue, ox + s.x * scale, oy + s.y * scale, scale * 0.24, s.angle, -s.image.width * 0.3 / self.tongue.width, 0.5))
}
commands.append(sprite(s.image, ox + s.x * scale, oy + s.y * scale, scale * 0.24, s.angle, ax, 0.5))
if i == 5 { commands.append(sprite(self.frontWing, ox + s.x * scale, oy + s.y * scale, scale * 0.24, s.angle - math.sin(self.time * 3 + 0.5) * 0.4, 1, 1)) }
}
i = 0
while i < self.flames.count {
p = self.flames.at(i)
image = p.art.image
imageScale = p.size * scale / math.max(image.width, image.height)
angle = math.atan2(p.vy, p.vx) - p.art.axis
drawing = sprite(image, ox + p.x * scale, oy + p.y * scale, imageScale, angle, 0.5, 0.5)
drawing.opacity = 1 - p.age / 12
if p.age > 4 { drawing.tint = "#E08A30" }
if p.age > 8 { drawing.tint = "#F0C030" }
commands.append(drawing)
i = i + 1
}
return commands
}
function heat(self: Dragon, commands: ObjectList, ox: Number, oy: Number, scale: Number): Unit {
i = 0
while i < commands.count {
c = commands.at(i)
j = 0
while j < self.flames.count {
p = self.flames.at(j)
dx = c.x - ox - p.x * scale
if math.abs(dx) < 60 * scale {
dy = c.y - oy - p.y * scale
if math.abs(dy) < 60 * scale {
distance = math.sqrt(dx * dx + dy * dy)
if distance > 0.1 {
if distance < 60 * scale {
weight = (1 - distance / (60 * scale)) * (1 - p.age / 12)
c.x = c.x + dx / distance * weight * 8 * scale
c.y = c.y + dy / distance * weight * 8 * scale
c.angle = c.angle + weight * 0.12
c.opacity = math.max(0.3, c.opacity - weight * 0.2)
c.tint = "#C4402A"
}
}
}
}
j = j + 1
}
i = i + 1
}
}
}
class Manuscript: RectBlock {
dragon: Dragon
page: Object
prepared: Object
small: Object
initial: Object
timer: Object
function measure(self: Manuscript, mount: Mount, limits: Constraints): MeasureResult {
return MeasureResult(math.min(limits.maxWidth, 1000), math.min(limits.maxHeight, 900))
}
function onMount(self: Manuscript, mount: Mount): Unit {
self.timer = ui.after(self.page, "animation", 80, self.tick)
}
function onUnmount(self: Manuscript, mount: Mount): Unit { self.timer.cancel() }
function tick(self: Manuscript): Unit {
self.dragon.step()
self.timer = ui.after(self.page, "animation", 80, self.tick)
}
function paint(self: Manuscript, mount: Mount, bounds: Bounds): DisplayList {
scale = math.max(0.2, math.min(1, (bounds.width - 40) / 700))
ox = bounds.x + (bounds.width - 700 * scale) / 2
oy = bounds.y + 30
margin = 45 * scale
text = self.prepared
line = 34
if bounds.width < 540 {
text = self.small
line = 26
}
initialHeight = line * 7
initialWidth = self.initial.width / self.initial.height * initialHeight
art = self.dragon.artwork(ox, oy, scale)
exclusions = ObjectList(g.rectExclusion(ox + margin, oy + margin, initialWidth + 12, initialHeight))
i = 0
while i < art.count {
exclusions.append(g.imageExclusion(art.at(i), 6 * scale))
i = i + 1
}
letters = 0
if self.dragon.flames.count > 0 { letters = 1 }
drawing = g.flowText(text, ox + margin, oy + margin, 700 * scale - margin * 2, math.max(0, bounds.height - 90 - margin), exclusions, letters)
if self.dragon.flames.count > 0 { self.dragon.heat(drawing.commands, ox, oy, scale) }
commands = RectBlock.paint(self, mount, bounds).commands
commands.append(g.sprite(self.initial, ox + margin, oy + margin, initialWidth, initialHeight, 0, 0, 0, 1))
commands.extend(drawing.commands)
commands.extend(art)
footer = g.flowText(hint, bounds.x + 20, bounds.y + bounds.height - 28, math.max(0, bounds.width - 40), 24, ObjectList(), 0)
commands.extend(footer.commands)
return DisplayList(commands)
}
}
segments = ObjectList()
headImage = g.loadImage(assets + "dragon-sprites/head.png")
segments.append(Segment(math.float(150), math.float(110), math.float(0), headImage))
function makeBody(): Unit {
i = 1
x = math.float(150)
y = math.float(110)
while i < 20 {
angle = -i / 19 * math.pi * 0.7
x = x - math.cos(angle) * 30
y = y - math.sin(angle) * 30
image = g.loadImage(assets + "dragon-sprites/body-" + g.string(i) + ".png")
segments.append(Segment(x, y, angle, image))
i = i + 1
}
}
makeBody()
fireImages = ObjectList()
// Layer 2..11: baseline directions differ; these values belong to this artwork.
fireDirections = ObjectList(Direction(-0.50), Direction(-0.30), Direction(-0.55), Direction(0.40), Direction(-1.35), Direction(-1.57), Direction(-1.57), Direction(0.05), Direction(-1.40), Direction(1.12))
function loadFire(): Unit {
i = 2
while i <= 11 {
image = g.loadImage(assets + "fire-sprites/Layer " + g.string(i) + ".png")
fireImages.append(FireArt(image, fireDirections.at(i - 2).angle))
i = i + 1
}
}
loadFire()
dragon = Dragon(segments, ObjectList(), fireImages, g.loadImage(assets + "dragon-sprites/tongue.png"), g.loadImage(assets + "dragon-sprites/wing-front.png"), g.loadImage(assets + "dragon-sprites/wing-back.png"), math.float(150), math.float(110), 0, math.now(), -10, math.float(42))
page = Page()
page.title = "Illustrated Manuscript · Endor"
view = Manuscript(700, 800, 0, "#F4EEE0", dragon, page, bodyText, smallText, g.loadImage(assets + "dropcap.png"), ObjectList())
view.flex = 1
view.boxWidth = "fill"
page.crossAlign = "stretch"
moveConnection = view.on("pointermove", dragon.pointer)
downConnection = view.on("pointerdown", dragon.down)
upConnection = view.on("pointerup", dragon.up)
leaveConnection = view.on("pointerleave", dragon.leave)
cancelConnection = view.on("pointercancel", dragon.leave)
page.render(view)