Add Blender worlds for Terra's close sweep, Horror and After Dark, and Terra's new logo
pauljd/davidson-ci: Davidson CI / quality (push) Failing after 1m44s
pauljd/davidson-ci: Davidson CI / ai-review (push) Failing after 1m22s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-02 17:36:35 +00:00
co-authored by Claude Opus 5.5
parent 05475584f7
commit b366610007
4 changed files with 265 additions and 8 deletions
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
"""Blood running down from the Horror logo, as frames to lay over the finished ident.
blood_drips.py <logo_word.png> <out folder>
"""
import random, sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFilter
word = Image.open(sys.argv[1]).convert('RGBA').getbbox()
out = Path(sys.argv[2]); out.mkdir(parents=True, exist_ok=True)
rng = random.Random(13)
# The logo is drawn 992 wide, centred, shifted as compose_ident.sh does. Scale from its 1600-wide canvas.
scale = 992 / 1600
ox, oy = (1280 - 992) / 2 - 110, (720 - 640 * scale) / 2 - 30
drips = []
for along in (.05, .19, .33, .47, .62, .76, .91):
drips.append(dict(x=ox + (word[0] + (word[2] - word[0]) * along) * scale, y=oy + (word[3] - 6) * scale, w=rng.choice((3, 4, 5)), reach=rng.randint(40, 150), start=rng.randint(245, 285), speed=rng.uniform(.8, 1.7)))
for frame in range(1, 351):
picture = Image.new('RGBA', (1280, 720))
draw = ImageDraw.Draw(picture)
for d in drips:
age = frame - d['start']
if age <= 0: continue
length = min(d['reach'], age * d['speed'])
x, y, w = d['x'], d['y'], d['w']
draw.rounded_rectangle((x - w / 2, y, x + w / 2, y + length), w / 2, fill=(150, 8, 16, 255))
draw.ellipse((x - w, y + length - w, x + w, y + length + w * 1.5), fill=(170, 10, 20, 255))
draw.line((x - w / 4, y, x - w / 4, y + length), fill=(232, 60, 60, 120), width=1)
picture.save(out / f'over_{frame:04d}.png')
+198 -7
View File
@@ -330,6 +330,187 @@ def tunnel():
hoop.scale = (1, 1 - .12 * ((i % 2) * 2 - 1) * .4, 1)
return tube
def sun_burst(position, size):
"""A burst of rays that always faces the camera, painted here rather than loaded, so the scene needs no files."""
side = 512
rng = random.Random(5)
rays = [(rng.uniform(0, math.tau), rng.uniform(.012, .035), rng.uniform(.5, 1)) for _ in range(11)]
pixels = [0.0] * (side * side * 4)
for row in range(side):
y = (row - side / 2) / (side / 2)
for col in range(side):
x = (col - side / 2) / (side / 2)
r = math.hypot(x, y) + 1e-5
a = math.atan2(y, x)
total = 0.0
for angle, width, reach in rays:
if r < reach:
off = abs((a - angle + math.pi) % math.tau - math.pi) / (width / (r + .06))
if off < 3: total += math.exp(-off * off) * (1 - r / reach) ** 1.4
v = min(4, math.exp(-(r / .045) ** 2) * 3 + math.exp(-(r / .2) ** 2) * .7 + math.exp(-(r / .6) ** 2) * .16 + total * 1.3)
at = (row * side + col) * 4
pixels[at], pixels[at + 1], pixels[at + 2], pixels[at + 3] = v, v * .66, v * .26, min(1, v)
image = bpy.data.images.new('burst', side, side, alpha=True, float_buffer=True)
image.pixels.foreach_set(pixels)
image.pack()
bpy.ops.mesh.primitive_plane_add(size=size, location=position); plane = bpy.context.active_object; plane.name = 'burst'
m, n, l = material('burst'); out = n['Material Output']; n.remove(n['Principled BSDF'])
tex = n.new('ShaderNodeTexImage'); tex.image = image
glow = n.new('ShaderNodeEmission'); glow.inputs['Strength'].default_value = 3
l.new(tex.outputs['Color'], glow.inputs['Color'])
clear = n.new('ShaderNodeBsdfTransparent'); mix = n.new('ShaderNodeMixShader')
l.new(tex.outputs['Alpha'], mix.inputs[0]); l.new(clear.outputs[0], mix.inputs[1]); l.new(glow.outputs[0], mix.inputs[2]); l.new(mix.outputs[0], out.inputs['Surface'])
plane.data.materials.append(m); plane.visible_shadow = False
return plane
def swirling_earth(radius=100):
"""Seen close: deep teal ocean with currents, cloud banks that catch the light, and a bright line of air."""
body = sphere('earth', radius, 512)
m, n, l = material('earth'); bsdf = n['Principled BSDF']; bsdf.inputs['Roughness'].default_value = .6; bsdf.inputs['Specular IOR Level'].default_value = .12
coord = n.new('ShaderNodeTexCoord')
current = noise_ramp(n, l, coord.outputs['Object'], .05, 14, [(.3, '#011722'), (.44, '#04334A'), (.54, '#0A5068'), (.6, '#3E8EA0'), (.66, '#073E54'), (.8, '#011722')], 5)
l.new(current.outputs['Color'], bsdf.inputs['Base Color']); body.data.materials.append(m)
spin(body, 'x', .0035)
clouds = sphere('clouds', radius * 1.006, 512)
m, n, l = material('clouds'); out = n['Material Output']
bsdf = n['Principled BSDF']; bsdf.inputs['Base Color'].default_value = (1, 1, 1, 1); bsdf.inputs['Roughness'].default_value = 1; bsdf.inputs['Specular IOR Level'].default_value = 0
coord = n.new('ShaderNodeTexCoord')
bands = n.new('ShaderNodeTexNoise'); bands.inputs['Scale'].default_value = .035; bands.inputs['Detail'].default_value = 15; bands.inputs['Distortion'].default_value = 3.2; bands.inputs['Roughness'].default_value = .62
l.new(coord.outputs['Object'], bands.inputs['Vector'])
cover = ramp(n, [(.52, '#000000'), (.6, '#FFFFFF')]); l.new(bands.outputs['Fac'], cover.inputs['Fac'])
puff = n.new('ShaderNodeTexNoise'); puff.inputs['Scale'].default_value = .9; puff.inputs['Detail'].default_value = 12; l.new(coord.outputs['Object'], puff.inputs['Vector'])
bump = n.new('ShaderNodeBump'); bump.inputs['Strength'].default_value = 1; bump.inputs['Distance'].default_value = .8
l.new(puff.outputs['Fac'], bump.inputs['Height']); l.new(bump.outputs[0], bsdf.inputs['Normal'])
clear = n.new('ShaderNodeBsdfTransparent'); mix = n.new('ShaderNodeMixShader')
l.new(cover.outputs['Color'], mix.inputs[0]); l.new(clear.outputs[0], mix.inputs[1]); l.new(bsdf.outputs[0], mix.inputs[2]); l.new(mix.outputs[0], out.inputs['Surface'])
clouds.data.materials.append(m); spin(clouds, 'x', .0045)
glow_shell('air', radius * 1.012, '#5FB4FF', 7, .1, 6)
def box(name, x, y, width, depth, height, mat, z=0):
half_x, half_y = width / 2, depth / 2
corners = [(a, b, c) for c in (0, height) for a, b in ((-half_x, -half_y), (half_x, -half_y), (half_x, half_y), (-half_x, half_y))]
faces = [(0, 3, 2, 1), (4, 5, 6, 7), (0, 1, 5, 4), (1, 2, 6, 5), (2, 3, 7, 6), (3, 0, 4, 7)]
mesh = bpy.data.meshes.new(name); mesh.from_pydata(corners, [], faces); mesh.update()
obj = bpy.data.objects.new(name, mesh); obj.location = (x, y, z); bpy.context.collection.objects.link(obj)
mesh.materials.append(mat)
return obj
def window_wall(wall, lit, window_colours, lit_share=.3, cell=(1.6, 2.4)):
"""A wall covered in windows, some lit, worked out from each building's own position, so no two match."""
m, n, l = material('wall'); bsdf = n['Principled BSDF']; bsdf.inputs['Base Color'].default_value = hexrgb(wall); bsdf.inputs['Roughness'].default_value = .7
coord = n.new('ShaderNodeTexCoord'); info = n.new('ShaderNodeObjectInfo')
sep = n.new('ShaderNodeSeparateXYZ'); l.new(coord.outputs['Object'], sep.inputs[0])
across = n.new('ShaderNodeMath'); across.operation = 'ADD'; l.new(sep.outputs['X'], across.inputs[0]); l.new(sep.outputs['Y'], across.inputs[1])
def cells(source, size):
div = n.new('ShaderNodeMath'); div.operation = 'DIVIDE'; div.inputs[1].default_value = size; l.new(source, div.inputs[0])
whole = n.new('ShaderNodeMath'); whole.operation = 'FLOOR'; l.new(div.outputs[0], whole.inputs[0])
part = n.new('ShaderNodeMath'); part.operation = 'FRACT'; l.new(div.outputs[0], part.inputs[0])
inner = n.new('ShaderNodeMapRange'); inner.interpolation_type = 'STEPPED'
low = n.new('ShaderNodeMath'); low.operation = 'GREATER_THAN'; low.inputs[1].default_value = .22; l.new(part.outputs[0], low.inputs[0])
high = n.new('ShaderNodeMath'); high.operation = 'LESS_THAN'; high.inputs[1].default_value = .78; l.new(part.outputs[0], high.inputs[0])
both = n.new('ShaderNodeMath'); both.operation = 'MULTIPLY'; l.new(low.outputs[0], both.inputs[0]); l.new(high.outputs[0], both.inputs[1])
return whole, both
col, in_x = cells(across.outputs[0], cell[0]); row, in_z = cells(sep.outputs['Z'], cell[1])
seed = n.new('ShaderNodeCombineXYZ'); l.new(col.outputs[0], seed.inputs[0]); l.new(row.outputs[0], seed.inputs[1]); l.new(info.outputs['Random'], seed.inputs[2])
white = n.new('ShaderNodeTexWhiteNoise'); white.noise_dimensions = '3D'; l.new(seed.outputs[0], white.inputs['Vector'])
on = n.new('ShaderNodeMath'); on.operation = 'LESS_THAN'; on.inputs[1].default_value = lit_share; l.new(white.outputs['Value'], on.inputs[0])
shape = n.new('ShaderNodeMath'); shape.operation = 'MULTIPLY'; l.new(in_x.outputs[0], shape.inputs[0]); l.new(in_z.outputs[0], shape.inputs[1])
show = n.new('ShaderNodeMath'); show.operation = 'MULTIPLY'; l.new(shape.outputs[0], show.inputs[0]); l.new(on.outputs[0], show.inputs[1])
tint = ramp(n, [(0, window_colours[0]), (.6, window_colours[1]), (1, window_colours[2])]); l.new(white.outputs['Color'], tint.inputs['Fac'])
amount = n.new('ShaderNodeMath'); amount.operation = 'MULTIPLY'; amount.inputs[1].default_value = lit
l.new(show.outputs[0], amount.inputs[0])
l.new(tint.outputs['Color'], bsdf.inputs['Emission Color']); l.new(amount.outputs[0], bsdf.inputs['Emission Strength'])
return m
def city(accent):
"""After Dark: a street of real towers at night, with neon along their edges."""
rng = random.Random(102)
wall = window_wall('#242E58', 1.6, ('#FF9A4A', '#FFD27A', '#9AC8FF'), .14, (1.1, 1.7))
neon, n, l = material('neon'); out = n['Material Output']; n.remove(n['Principled BSDF'])
glow = n.new('ShaderNodeEmission'); glow.inputs['Color'].default_value = hexrgb(accent); glow.inputs['Strength'].default_value = 14; l.new(glow.outputs[0], out.inputs['Surface'])
road, n, l = material('road'); bsdf = n['Principled BSDF']; bsdf.inputs['Base Color'].default_value = hexrgb('#05070F'); bsdf.inputs['Roughness'].default_value = .14; bsdf.inputs['Metallic'].default_value = .3
box('road', 0, 150, 400, 700, .1, road, -.1)
for row, y in enumerate(range(0, 320, 26)):
for side in (-1, 1):
for far in range(3):
x = side * (30 + far * 26 + rng.uniform(-3, 3))
width, depth, height = rng.uniform(14, 22), rng.uniform(14, 22), rng.uniform(22, 70) + far * 14
box(f'tower{row}{side}{far}', x, y + rng.uniform(-4, 4), width, depth, height, wall)
if far == 0 and rng.random() < .55:
inner = x - side * width / 2
box(f'neon{row}{side}', inner, y - depth * .45, .35, .35, height, neon)
for x in range(-330, 330, 34):
box(f'back{x}', x, 340 + rng.uniform(0, 50), 30, 30, rng.uniform(60, 180), wall)
# A low moon at the end of the street, in the accent's glow.
flare((54, 600, 205), 30, '#E8E4F5', 1.5)
# Lamps along the road, for reflections.
lamp, n, l = material('lamp'); out = n['Material Output']; n.remove(n['Principled BSDF'])
glow = n.new('ShaderNodeEmission'); glow.inputs['Color'].default_value = hexrgb('#FFC873'); glow.inputs['Strength'].default_value = 20; l.new(glow.outputs[0], out.inputs['Surface'])
for y in range(-20, 320, 22):
for side in (-1, 1):
box(f'lamp{y}{side}', side * 9, y, .4, .4, .3, lamp, 7)
data = bpy.data.lights.new('fill', 'SUN'); data.energy = 1.8; data.color = hexrgb('#8FA3F0')[:3]; fill = bpy.data.objects.new('fill', data); bpy.context.collection.objects.link(fill); fill.rotation_euler = (math.radians(60), 0, math.radians(30))
def dead_tree(x, y, z, seed, mat, scale=1.0):
"""A bare tree, grown from branching tubes."""
rng = random.Random(seed)
curve = bpy.data.curves.new(f'tree{seed}', 'CURVE'); curve.dimensions = '3D'; curve.bevel_depth = .12 * scale; curve.bevel_resolution = 2; curve.use_fill_caps = True
def grow(start, direction, length, thick, depth):
end = start + direction * length
spline = curve.splines.new('POLY'); spline.points.add(1)
spline.points[0].co = (*start, 1); spline.points[0].radius = thick
spline.points[1].co = (*end, 1); spline.points[1].radius = thick * .62
if depth:
for _ in range(rng.randint(2, 3)):
turn = Vector((rng.uniform(-.8, .8), rng.uniform(-.8, .8), rng.uniform(.0, .8))).normalized()
grow(end, (direction + turn).normalized(), length * rng.uniform(.6, .8), thick * .62, depth - 1)
grow(Vector((0, 0, 0)), Vector((0, 0, 1)), 3.4 * scale, 2.4, 5)
obj = bpy.data.objects.new(f'tree{seed}', curve); obj.location = (x, y, z); bpy.context.collection.objects.link(obj); curve.materials.append(mat)
def haunted(windows_colour='#FF2A1E'):
"""Horror: a real house on a hill under a blood moon, its windows flickering."""
black, n, l = material('black'); bsdf = n['Principled BSDF']; bsdf.inputs['Base Color'].default_value = hexrgb('#16101A'); bsdf.inputs['Roughness'].default_value = .85
wall, n, l = material('housewall'); bsdf = n['Principled BSDF']; bsdf.inputs['Base Color'].default_value = hexrgb('#3A2A3C'); bsdf.inputs['Roughness'].default_value = .9
def roof(x, y, z, width, depth, high):
half_x, half_y = width / 2, depth / 2
corners = [(-half_x, -half_y, 0), (half_x, -half_y, 0), (half_x, half_y, 0), (-half_x, half_y, 0), (0, 0, high)]
mesh = bpy.data.meshes.new('roof'); mesh.from_pydata(corners, [], [(0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4), (3, 2, 1, 0)]); mesh.update()
obj = bpy.data.objects.new('roof', mesh); obj.location = (x, y, z); bpy.context.collection.objects.link(obj); mesh.materials.append(black)
hx, hy = 16, 30
box('main', hx, hy, 18, 11, 11, wall); roof(hx, hy, 11, 20, 13, 8)
box('wing', hx - 11.5, hy + 1, 8, 9, 7.5, wall); roof(hx - 11.5, hy + 1, 7.5, 10, 11, 5.5)
box('tower', hx + 10.5, hy - 1, 5.5, 5.5, 16, wall); roof(hx + 10.5, hy - 1, 16, 7.5, 7.5, 10)
box('chimney', hx - 4, hy + 2, 1.6, 1.6, 21, black)
glow, n, l = material('windows'); out = n['Material Output']; n.remove(n['Principled BSDF'])
emit = n.new('ShaderNodeEmission'); emit.inputs['Color'].default_value = hexrgb(windows_colour); l.new(emit.outputs[0], out.inputs['Surface'])
rng = random.Random(4); frame = 1
while frame < LENGTH:
emit.inputs['Strength'].default_value = rng.choice((7, 7, 6, 4, 1.6, .5)); emit.inputs['Strength'].keyframe_insert('default_value', frame=frame)
frame += rng.randint(2, 11)
front = hy - 5.6
for x, z, w, h in ((hx - 6, 7, 1.7, 2.6), (hx, 7, 1.7, 2.6), (hx + 6, 7, 1.7, 2.6), (hx - 6, 2, 1.7, 2.6), (hx + 6, 2, 1.7, 2.6), (hx - 11.5, 3, 1.5, 2.3)):
box('win', x, front - (.0 if abs(x - hx) < 8 else -1.2), w, .1, h, glow, z)
box('towerwin', hx + 10.5, hy - 3.85, 1.4, .1, 2.3, glow, 11); box('towerwin2', hx + 10.5, hy - 3.85, 1.4, .1, 2.3, glow, 6)
box('door', hx, front, 2.4, .1, 3.6, black)
# The hill, real ground rising to the house.
bpy.ops.mesh.primitive_grid_add(x_subdivisions=300, y_subdivisions=300, size=400, location=(0, 60, 0)); hill = bpy.context.active_object; bpy.ops.object.shade_smooth()
for v in hill.data.vertices:
x, y = v.co.x, v.co.y + 60
v.co.z = -2.5 + 3.2 * math.exp(-(((x - hx) / 34) ** 2 + ((y - hy) / 34) ** 2)) + .35 * math.sin(x * .13) * math.cos(y * .11) + .18 * math.sin(x * .41 + y * .37)
hill.data.update(); hill.data.materials.append(black)
for x, y, seed, s in ((-28, 8, 1, 1.7), (-14, 18, 2, 1.1), (-34, 30, 3, 1.3), (4, 2, 4, .9), (-6, -8, 6, 1.5)):
dead_tree(x, y, -2.2, seed, black, s)
# Blood moon behind the house.
moon, n, l = material('moon'); out = n['Material Output']; n.remove(n['Principled BSDF'])
coord = n.new('ShaderNodeTexCoord'); tone = noise_ramp(n, l, coord.outputs['Object'], .22, 10, [(.32, '#7A0A10'), (.7, '#E8261E')])
emit = n.new('ShaderNodeEmission'); l.new(tone.outputs['Color'], emit.inputs['Color']); emit.inputs['Strength'].default_value = 2.4; l.new(emit.outputs[0], out.inputs['Surface'])
ball = sphere('moon', 26); ball.location = (46, 150, 40); ball.data.materials.append(moon); ball.visible_shadow = False
data = bpy.data.lights.new('rim', 'SUN'); data.energy = 1.5; data.color = hexrgb('#FF7A6A')[:3]; rim = bpy.data.objects.new('rim', data); bpy.context.collection.objects.link(rim); rim.rotation_euler = Vector((.45, .75, -.5)).to_track_quat('-Z', 'Y').to_euler()
scene.world = bpy.data.worlds.new('fog'); scene.world.use_nodes = True
bg = scene.world.node_tree.nodes['Background']; bg.inputs['Strength'].default_value = 1.1
w = scene.world.node_tree; tex = w.nodes.new('ShaderNodeTexCoord'); sep = w.nodes.new('ShaderNodeSeparateXYZ'); w.links.new(tex.outputs['Generated'], sep.inputs[0])
sky = ramp(w.nodes, [(.47, '#7A2030'), (.56, '#3A1020'), (.72, '#0E0614')]); w.links.new(sep.outputs['Z'], sky.inputs['Fac']); w.links.new(sky.outputs['Color'], bg.inputs['Color'])
if SCENE == 'orbit-1':
world_tint = ('#061A3C', '#0E3A6E')
stars_world()
@@ -343,13 +524,14 @@ elif SCENE == 'paradox':
hole, _ = black_hole()
camera_path([(1, (14, -150, 12), (0, 0, 0)), (260, (-19, -66, 8), (-14, 0, 1)), (350, (-19, -66, 8), (-14, 0, 1))], lens=38)
elif SCENE == 'orbit-terra':
world_tint = ('#020612', '#06122A')
world_tint = ('#020409', '#040812')
stars_world()
# The sun comes up behind the world's edge, as the camera sweeps down to it.
sun((-.75, .62, -.1), 5, '#FFF2DC', 1)
earth()
flare((60, -40, 12), 2.2, '#FFE9C4', 60)
camera_path([(1, (-26, -8, 26), (0, 0, 0)), (260, (-20, -29, 3), (-8.6, 0, 3.2)), (350, (-20, -29, 3), (-8.6, 0, 3.2))], lens=40)
swirling_earth()
# Low over the ocean, sweeping across towards the sun as it breaks over the edge of the world.
sun((-.05, -.62, -.78), 5.5, '#FFF0DA', 1)
burst = sun_burst((26, 400, -78), 520)
camera_path([(1, (-46, 0, 104), (-30, 100, 24)), (290, (16, 0, 111), (16, 100, 26)), (350, (20, 0, 111), (18, 100, 26))], lens=30)
track = burst.constraints.new('TRACK_TO'); track.target = scene.camera; track.track_axis = 'TRACK_Z'; track.up_axis = 'UP_Y'
elif SCENE == 'orbit-odyssey':
world_tint = ('#020409', '#050A18')
stars_world()
@@ -359,8 +541,17 @@ elif SCENE == 'orbit-odyssey':
bpy.ops.object.select_all(action='DESELECT')
planet_body = earth()
for name in ('earth', 'clouds', 'air'):
bpy.data.objects[name].location = (26, 150, 30); bpy.data.objects[name].scale = (1.5, 1.5, 1.5)
bpy.data.objects[name].location = (62, 150, 42); bpy.data.objects[name].scale = (1.5, 1.5, 1.5)
camera_path([(1, (-6, -62, 6), (0, 40, 3)), (260, (-2, -20, 7), (3, 60, 17)), (350, (-2, -20, 7), (3, 60, 17))], lens=30)
elif SCENE == 'orbit-horror':
haunted()
camera_path([(1, (-52, -30, 3), (8, 30, 10)), (280, (-13, -19, 2.4), (-2, 30, 12)), (350, (-13, -19, 2.4), (-2, 30, 12))], lens=26)
elif SCENE == 'orbit-after-dark':
world_tint = ('#1A2868', '#5A2A90')
stars_world()
city('#E84CCB')
# Rises up the street, between the towers, to the moon.
camera_path([(1, (0, -70, 4), (0, 80, 16)), (290, (4, 90, 44), (12, 400, 96)), (350, (4, 98, 45), (12, 400, 96))], lens=26)
elif SCENE == 'vortex':
world_tint = ('#020409', '#060A18')
scene.world = bpy.data.worlds.new('dark'); scene.world.use_nodes = True; scene.world.node_tree.nodes['Background'].inputs['Color'].default_value = (0, 0, 0, 1)
+7 -1
View File
@@ -1,6 +1,12 @@
#!/bin/bash
# Frames from build_3d_ident.py into a finished ident: a soft start, then the channel's logo fading in over the end.
# compose_ident.sh <frames folder> <logo png> <out.mp4>
# compose_ident.sh <frames folder> <logo png> <out.mp4> [overlay folder of over_%04d.png, drawn on top]
if [ -n "$4" ]; then
ffmpeg -nostdin -y -loglevel error -framerate 50 -i "$1/frame_%04d.png" -loop 1 -i "$2" -framerate 50 -i "$4/over_%04d.png" -filter_complex \
"[1:v]scale=992:-1,format=rgba,fade=t=in:st=4.4:d=0.8:alpha=1[logo];[0:v]fade=t=in:st=0:d=0.3[scene];[scene][logo]overlay=x=(W-w)/2-110:y=(H-h)/2-30:shortest=1[lit];[lit][2:v]overlay=0:0:shortest=1,format=yuv420p" \
-t 7 -c:v libx264 -preset slow -crf 18 -movflags +faststart -an "$3"
exit
fi
ffmpeg -nostdin -y -loglevel error -framerate 50 -i "$1/frame_%04d.png" -loop 1 -i "$2" -filter_complex \
"[1:v]scale=992:-1,format=rgba,fade=t=in:st=4.4:d=0.8:alpha=1[logo];[0:v]fade=t=in:st=0:d=0.3[scene];[scene][logo]overlay=x=(W-w)/2-110:y=(H-h)/2-30:shortest=1,format=yuv420p" \
-t 7 -c:v libx264 -preset slow -crf 18 -movflags +faststart -an "$3"
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""A more natural Terra logo for the 3D ident, with a real-looking leaf. The approved logo files stay as they are.
terra_logo.py <out.png>
"""
import math, os, subprocess, sys
from pathlib import Path
ENV = dict(os.environ, XDG_DATA_HOME=str(Path(__file__).parents[3] / '.tooling/font-data'))
CRESCENT = 'M335 132C141 114 66 384 257 475C143 363 185 207 335 132Z'
RING = 'M129 369C102 399 111 425 161 428C267 432 431 340 445 289C455 250 405 250 369 261'
defs = ('<linearGradient id="silver" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#FFFFFF"/><stop offset=".44" stop-color="#E8EDF5"/><stop offset=".5" stop-color="#B7CBE8"/><stop offset="1" stop-color="#F5F7FF"/></linearGradient>'
'<linearGradient id="word" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#FFFFFF"/><stop offset=".55" stop-color="#E4EAF0"/><stop offset="1" stop-color="#B9C7D2"/></linearGradient>'
'<linearGradient id="leaf" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#8FCB4A"/><stop offset=".5" stop-color="#3F8F2E"/><stop offset="1" stop-color="#1C5A22"/></linearGradient>'
'<linearGradient id="shade" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#FFFFFF" stop-opacity=".28"/><stop offset=".5" stop-color="#FFFFFF" stop-opacity="0"/><stop offset=".5" stop-color="#000000" stop-opacity=".14"/><stop offset="1" stop-color="#000000" stop-opacity=".05"/></linearGradient>'
'<filter id="soft" x="-20%" y="-40%" width="140%" height="180%"><feGaussianBlur stdDeviation="16"/></filter>')
# The leaf, drawn pointing up along its midrib, with side veins and a stalk.
veins = ''
for i in range(1, 7):
y = 150 - i * 21
spread = 50 * math.sin(math.pi * (i + .6) / 8)
veins += f'<path d="M0 {y}Q{spread * .6:.0f} {y - 14} {spread:.0f} {y - 30}M0 {y}Q{-spread * .6:.0f} {y - 14} {-spread:.0f} {y - 30}" fill="none" stroke="#D7F0A8" stroke-opacity=".55" stroke-width="2.4"/>'
leaf = ('<path d="M0 -40C70 -2 78 96 0 176C-78 96 -70 -2 0 -40Z" fill="url(#leaf)"/><path d="M0 -40C70 -2 78 96 0 176C-78 96 -70 -2 0 -40Z" fill="url(#shade)"/>' + veins +
'<path d="M0 -36V206" fill="none" stroke="#E4F5BE" stroke-opacity=".8" stroke-width="3.5" stroke-linecap="round"/>')
word = 'font-family="Barlow" font-weight="400" letter-spacing="26"'
body = (f'<g opacity=".7" filter="url(#soft)"><text x="490" y="430" font-size="200" {word} fill="#02060C">TERRA</text><path d="{CRESCENT}" fill="#02060C"/></g>'
f'<path d="{CRESCENT}" fill="url(#silver)"/><path d="{RING}" fill="none" stroke="#BFE3C8" stroke-width="10"/><circle cx="430" cy="282" r="10" fill="#BFE3C8"/>'
f'<text x="494" y="180" font-family="Barlow" font-weight="600" font-size="52" letter-spacing="14" fill="#F5F7FF">ORBIT</text>'
f'<text x="490" y="430" font-size="200" {word} fill="url(#word)">TERRA</text>'
f'<g transform="translate(1235,190) rotate(32) scale(.62)">{leaf}</g>')
out = Path(sys.argv[1])
src = out.with_suffix('.svg')
src.write_text(f'<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="640" viewBox="0 0 1600 640"><defs>{defs}</defs>{body}</svg>')
subprocess.run(['vectorcraft-cli', 'convert', str(src), str(out)], env=ENV, check=True, stdout=subprocess.DEVNULL)