298 lines
12 KiB
Plaintext
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)
|