diff --git a/design/channels/blender/blood_drips.py b/design/channels/blender/blood_drips.py new file mode 100644 index 0000000..449d92d --- /dev/null +++ b/design/channels/blender/blood_drips.py @@ -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 +""" +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') diff --git a/design/channels/blender/build_3d_ident.py b/design/channels/blender/build_3d_ident.py index f7cc762..fdc3b79 100644 --- a/design/channels/blender/build_3d_ident.py +++ b/design/channels/blender/build_3d_ident.py @@ -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) diff --git a/design/channels/blender/compose_ident.sh b/design/channels/blender/compose_ident.sh index 4b32f57..9b48d5c 100755 --- a/design/channels/blender/compose_ident.sh +++ b/design/channels/blender/compose_ident.sh @@ -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 +# compose_ident.sh [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" diff --git a/design/channels/blender/terra_logo.py b/design/channels/blender/terra_logo.py new file mode 100644 index 0000000..f275e40 --- /dev/null +++ b/design/channels/blender/terra_logo.py @@ -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 +""" +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 = ('' + '' + '' + '' + '') +# 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'' +leaf = ('' + veins + + '') +word = 'font-family="Barlow" font-weight="400" letter-spacing="26"' +body = (f'TERRA' + f'' + f'ORBIT' + f'TERRA' + f'{leaf}') +out = Path(sys.argv[1]) +src = out.with_suffix('.svg') +src.write_text(f'{defs}{body}') +subprocess.run(['vectorcraft-cli', 'convert', str(src), str(out)], env=ENV, check=True, stdout=subprocess.DEVNULL)