Answer the image's requests on a task and signal a semaphore once done, so only the asking process waits.
650 lines
32 KiB
Swift
650 lines
32 KiB
Swift
import Foundation
|
|
|
|
public struct PharoExample: Identifiable, Sendable {
|
|
public var id: String { title }
|
|
public let title: String
|
|
public let code: String
|
|
}
|
|
|
|
public enum PharoExampleCatalog {
|
|
public static let sections: [(heading: String, examples: [PharoExample])] = [
|
|
("Visualize", [
|
|
PharoExample(
|
|
title: "Graph — a ring",
|
|
code: """
|
|
| graph |
|
|
graph := Mondrian new.
|
|
graph nodes with: (1 to: 8).
|
|
graph edges connectTo: [ :n | (n rem: 8) + 1 ].
|
|
graph layout circle.
|
|
graph
|
|
"""),
|
|
PharoExample(
|
|
title: "Graph — a class tree",
|
|
code: """
|
|
| graph |
|
|
graph := Mondrian new.
|
|
graph nodes with: Number withAllSubclasses.
|
|
graph edges connectFrom: [ :aClass | aClass superclass ].
|
|
graph layout horizontalTree.
|
|
graph
|
|
"""),
|
|
PharoExample(
|
|
title: "Bar chart",
|
|
code: """
|
|
GtPlotter new horizontalBarChart
|
|
barWidthData: [ :each | each ];
|
|
with: (GtPlotterDataGroup new
|
|
values: #( 3 7 5 9 2 6 );
|
|
labelled: [ :each | 'item ' , each printString ];
|
|
yourself);
|
|
yourself.
|
|
"""),
|
|
PharoExample(
|
|
title: "Line chart",
|
|
code: """
|
|
GtPlotterLineChart new
|
|
with: (GtPlotterDataGroup new values: (1 to: 12));
|
|
valueX: [ :n | n ];
|
|
valueY: [ :n | n * n ];
|
|
yourself.
|
|
"""),
|
|
PharoExample(
|
|
title: "Scatter plot",
|
|
code: """
|
|
GtPlotterScatterChart new
|
|
with: (GtPlotterDataGroup new values: (1 to: 30));
|
|
valueX: [ :n | n ];
|
|
valueY: [ :n | (n rem: 7) + n // 3 ];
|
|
yourself.
|
|
"""),
|
|
]),
|
|
("Draw", [
|
|
PharoExample(
|
|
title: "A scene of your own, among an object's views",
|
|
code: """
|
|
"The scene is the host's; the image keeps its handle and says
|
|
which one to draw. Inspect the answer to find the Scene tab.
|
|
shape stays put for the two snippets that change it."
|
|
scene := LumaCanvas new.
|
|
shape := scene addDrawable.
|
|
shape
|
|
attribute: 'p' components: 3;
|
|
varying: 'vc' components: 3;
|
|
uniform: 'tint' value: #(1 0.4 0.2);
|
|
vertexSource: 'void main() {
|
|
vc = vec3(0.5) + p * 0.5;
|
|
gl_Position = u_mvp * vec4(p, 1.0); }'
|
|
fragmentSource: 'void main() { frag_color = vec4(vc * tint, 1.0); }';
|
|
identity;
|
|
mesh: #(-0.8 -0.6 0 0.8 -0.6 0 0 0.8 0) primitive: #triangles.
|
|
scene
|
|
"""),
|
|
PharoExample(
|
|
title: "Say what a value should do, and leave it to draw",
|
|
code: """
|
|
"Run the scene example first. Nothing here runs per frame: the
|
|
shape is told the shape of the change, and whichever renderer
|
|
draws it works the value out on its own clock."
|
|
shape oscillate: 'tint' between: #(1 0.4 0.2) and: #(0.2 0.4 1) period: 3
|
|
"""),
|
|
PharoExample(
|
|
title: "Picture a run of values too large for a uniform",
|
|
code: """
|
|
"A buffer is read by index through <name>At, held as a texture,
|
|
so handing over a fresh window is all a scrub costs."
|
|
profileScene := LumaCanvas new.
|
|
profile := profileScene addDrawable.
|
|
profile
|
|
attribute: 'p' components: 2;
|
|
varying: 'uv' components: 2;
|
|
buffer: 'samples' values: ((0 to: 255) collect: [ :i | (i / 40.0) sin abs ]);
|
|
vertexSource: 'void main() {
|
|
uv = p * 0.5 + 0.5;
|
|
gl_Position = vec4(p, 0.0, 1.0); }'
|
|
fragmentSource: 'void main() {
|
|
float v = samplesAt(int(uv.x * 255.0));
|
|
frag_color = vec4(uv.y < v ? vec3(0.2, 0.9, 0.5) : vec3(0.09), 1.0); }';
|
|
mesh: #(-1 -1 1 -1 -1 1 1 -1 1 1 -1 1) primitive: #triangles.
|
|
profileScene
|
|
"""),
|
|
PharoExample(
|
|
title: "Hide a drawable, and bring it back",
|
|
code: """
|
|
"A scene is kept, not rebuilt: changing one part leaves the
|
|
rest, and its shaders, alone."
|
|
shape hide.
|
|
"then, separately: shape show"
|
|
"""),
|
|
PharoExample(
|
|
title: "Your sessions, as icons on turning cards",
|
|
code: """
|
|
"A picture is any Form, so what the host already carries -- a
|
|
session's icon -- goes straight onto a card of its own."
|
|
iconScene := LumaCanvas new.
|
|
shown := LumaProject sessions items select: [ :each | each icon notNil ].
|
|
shown doWithIndex: [ :session :index |
|
|
| angle card |
|
|
angle := 2 * Float pi * (index - 1) / shown size.
|
|
card := iconScene addDrawable.
|
|
card
|
|
attribute: 'p' components: 2;
|
|
varying: 'uv' components: 2;
|
|
uniform: 'centre' value: { angle sin * 0.55. angle cos * 0.55 };
|
|
image: 'icon' form: session icon;
|
|
vertexSource: 'void main() {
|
|
uv = vec2(p.x * 0.5 + 0.5, 0.5 - p.y * 0.5);
|
|
vec2 square = vec2(min(u_resolution.y / u_resolution.x, 1.0),
|
|
min(u_resolution.x / u_resolution.y, 1.0));
|
|
gl_Position = u_mvp * vec4((p * 0.2 + centre) * square, 0.0, 1.0); }'
|
|
fragmentSource: 'void main() {
|
|
vec4 c = texture(icon, uv);
|
|
if (c.a < 0.01) discard;
|
|
frag_color = vec4(c.rgb * c.a, c.a); }';
|
|
mesh: #(-1 -1 1 -1 -1 1 1 -1 1 1 -1 1) primitive: #triangles;
|
|
oscillate: 'centre'
|
|
between: { angle sin * 0.55. angle cos * 0.55 }
|
|
and: { angle sin * 0.7. angle cos * 0.7 }
|
|
period: 4 ].
|
|
iconScene
|
|
"""),
|
|
PharoExample(
|
|
title: "Icons on parade, with a tune under them",
|
|
code: """
|
|
"Three channels and a ring of cards, started together. Hush
|
|
with: LumaTune hush"
|
|
LumaSynth start.
|
|
(LumaTune named: #lead channel: 0)
|
|
patch: #pulse; tempo: 150;
|
|
notes: #(c5 e5 g5 e5 a5 g5 e5 c5 d5 f5 a5 f5 g5 e5 c5 -);
|
|
play.
|
|
(LumaTune named: #bass channel: 1)
|
|
patch: #bass; tempo: 150; division: 2;
|
|
notes: #(c2 - g2 - a2 - f2 -);
|
|
play.
|
|
(LumaTune named: #drums channel: 2)
|
|
patch: #noiseHit; tempo: 150;
|
|
notes: #(c5 - c5 c5 - c5 c5 -);
|
|
play.
|
|
|
|
parade := LumaCanvas new.
|
|
shown := LumaProject sessions items select: [ :each | each icon notNil ].
|
|
shown doWithIndex: [ :session :index |
|
|
| angle card |
|
|
angle := 2 * Float pi * (index - 1) / shown size.
|
|
card := parade addDrawable.
|
|
card
|
|
attribute: 'p' components: 2;
|
|
varying: 'uv' components: 2;
|
|
uniform: 'centre' value: { angle sin * 0.55. angle cos * 0.55 };
|
|
image: 'icon' form: session icon;
|
|
vertexSource: 'void main() {
|
|
uv = vec2(p.x * 0.5 + 0.5, 0.5 - p.y * 0.5);
|
|
vec2 square = vec2(min(u_resolution.y / u_resolution.x, 1.0),
|
|
min(u_resolution.x / u_resolution.y, 1.0));
|
|
float beat = 1.0 + 0.12 * sin(u_time * 5.0);
|
|
gl_Position = vec4((p * 0.2 * beat + centre) * square, 0.0, 1.0); }'
|
|
fragmentSource: 'void main() {
|
|
vec4 c = texture(icon, uv);
|
|
if (c.a < 0.01) discard;
|
|
frag_color = vec4(c.rgb * c.a, c.a); }';
|
|
mesh: #(-1 -1 1 -1 -1 1 1 -1 1 1 -1 1) primitive: #triangles;
|
|
oscillate: 'centre'
|
|
between: { angle sin * 0.5. angle cos * 0.5 }
|
|
and: { angle sin * 0.72. angle cos * 0.72 }
|
|
period: 3.2 ].
|
|
parade
|
|
"""),
|
|
PharoExample(
|
|
title: "Catch \u{2014} a game of pointer, keys and blips",
|
|
code: """
|
|
"Inspect the answer, click the Scene tab to give it the keys,
|
|
then steer with the pointer or the arrow keys. Escape ends
|
|
it, as does: playing terminate"
|
|
LumaSynth start.
|
|
game := LumaCanvas new.
|
|
icons := (LumaProject sessions items
|
|
select: [ :each | each icon notNil ]
|
|
thenCollect: [ :each | each icon ])
|
|
ifEmpty: [ { (Form extent: 64 @ 64 depth: 32)
|
|
fillColor: Color magenta; yourself } ].
|
|
|
|
"Sizes are fitted to the pane, positions are not, so what the
|
|
pointer reads is where things are."
|
|
fit := 'vec2 fit = vec2(min(u_resolution.y / u_resolution.x, 1.0),
|
|
min(u_resolution.x / u_resolution.y, 1.0));'.
|
|
quad := #(-1 -1 1 -1 -1 1 1 -1 1 1 -1 1).
|
|
lamp := 'const vec3 lamp = vec3(0.40, 0.58, 0.71);
|
|
float shine(vec3 n, float tightness) {
|
|
return pow(max(dot(reflect(-lamp, n), vec3(0.0, 0.0, 1.0)), 0.0), tightness); }'.
|
|
|
|
"Behind everything: a grid running to a horizon, which is
|
|
where the depth in the picture comes from. Depth runs 0 near
|
|
to 1 far -- Metal clips anything behind zero."
|
|
(game addDrawable)
|
|
attribute: 'p' components: 2;
|
|
varying: 'uv' components: 2;
|
|
vertexSource: 'void main() {
|
|
uv = p; gl_Position = vec4(p, 0.9, 1.0); }'
|
|
fragmentSource: 'void main() {
|
|
float horizon = 0.35;
|
|
vec3 sky = mix(vec3(0.32, 0.06, 0.36), vec3(0.04, 0.02, 0.12),
|
|
smoothstep(horizon, 1.0, uv.y));
|
|
sky += vec3(1.0, 0.4, 0.25)
|
|
* smoothstep(0.42, 0.0, length(vec2(uv.x, (uv.y - 0.5) * 1.7)));
|
|
vec3 col = sky;
|
|
if (uv.y < horizon) {
|
|
float below = horizon - uv.y;
|
|
float run = 0.09 / max(below, 0.004);
|
|
vec2 cell = abs(fract(vec2(uv.x * run, u_time * 1.2 + run)) - 0.5);
|
|
float line = smoothstep(0.07, 0.0, min(cell.x, cell.y));
|
|
col = mix(vec3(0.05, 0.02, 0.09), vec3(0.95, 0.3, 0.8), line);
|
|
col = mix(col, sky, exp(-below * 7.0));
|
|
}
|
|
frag_color = vec4(col, 1.0); }';
|
|
mesh: quad primitive: #triangles.
|
|
|
|
"What the faller throws on the floor: the lower it is, the
|
|
tighter and darker."
|
|
shadow := game addDrawable.
|
|
shadow
|
|
attribute: 'p' components: 2;
|
|
varying: 'uv' components: 2;
|
|
uniform: 'at' value: #(0 -0.88);
|
|
uniform: 'spread' value: 1;
|
|
vertexSource: 'void main() { uv = p; ', fit, '
|
|
gl_Position = vec4(p * vec2(0.16, 0.05) * spread * fit + at,
|
|
0.5, 1.0); }'
|
|
fragmentSource: 'void main() {
|
|
float a = smoothstep(1.0, 0.15, length(uv)) * 0.55 / spread;
|
|
frag_color = vec4(0.0, 0.0, 0.0, a); }';
|
|
mesh: quad primitive: #triangles.
|
|
|
|
"The faller turns on its own axis, and is lit as though it had
|
|
a face to catch the light."
|
|
faller := game addDrawable.
|
|
faller
|
|
attribute: 'p' components: 2;
|
|
varying: 'uv' components: 2;
|
|
varying: 'facing' components: 1;
|
|
uniform: 'at' value: #(0 1);
|
|
image: 'icon' form: icons first;
|
|
vertexSource: 'void main() {
|
|
uv = vec2(p.x * 0.5 + 0.5, 0.5 - p.y * 0.5); ', fit, '
|
|
float turn = u_time * 2.1;
|
|
vec3 local = vec3(p.x * cos(turn), p.y, p.x * sin(turn));
|
|
facing = cos(turn);
|
|
float near = 1.0 / (1.0 - local.z * 0.3);
|
|
gl_Position = vec4(local.xy * 0.14 * near * fit + at, 0.3, 1.0); }'
|
|
fragmentSource: lamp, '
|
|
void main() {
|
|
vec2 face = vec2(facing < 0.0 ? 1.0 - uv.x : uv.x, uv.y);
|
|
vec4 c = texture(icon, face);
|
|
if (c.a < 0.02) discard;
|
|
vec2 q = uv * 2.0 - 1.0;
|
|
vec3 n = normalize(vec3(q * 0.6, 1.0));
|
|
vec3 lit = c.rgb * (0.35 + 0.8 * max(dot(n, lamp), 0.0))
|
|
+ vec3(1.0, 0.95, 0.85) * shine(n, 26.0) * 0.6;
|
|
frag_color = vec4(lit * c.a, c.a); }';
|
|
mesh: quad primitive: #triangles.
|
|
|
|
paddle := game addDrawable.
|
|
paddle
|
|
attribute: 'p' components: 2;
|
|
varying: 'uv' components: 2;
|
|
uniform: 'at' value: #(0 -0.8);
|
|
uniform: 'warmth' value: 0;
|
|
vertexSource: 'void main() { uv = p; ', fit, '
|
|
gl_Position = vec4(p * vec2(0.22, 0.045) * fit + at, 0.2, 1.0); }'
|
|
fragmentSource: lamp, '
|
|
void main() {
|
|
vec3 n = normalize(vec3(uv.x * 0.2, uv.y * 0.9, 1.0));
|
|
vec3 base = mix(vec3(0.15, 0.85, 0.55), vec3(1.0, 0.7, 0.2), warmth);
|
|
float edge = smoothstep(1.0, 0.72, abs(uv.y));
|
|
vec3 lit = base * (0.4 + 0.7 * max(dot(n, lamp), 0.0))
|
|
+ vec3(1.0) * shine(n, 40.0) * (0.4 + warmth);
|
|
frag_color = vec4(lit * edge, edge); }';
|
|
mesh: quad primitive: #triangles.
|
|
|
|
"Half sizes, so a catch is judged on the edges rather than
|
|
the centres, and on the step the faller crosses the paddle."
|
|
tally := LumaText on: game addDrawable pointSize: 34.
|
|
tally pad: #(20 18) tint: #(1 0.95 0.8); show: 'SCORE 0'.
|
|
|
|
fallerHalf := 0.14. paddleHalf := 0.22. paddleTop := -0.755.
|
|
"Speeds are per second, not per turn round the loop: a turn
|
|
that runs late has to cover more ground, or the fall is seen
|
|
to slow down whenever the image is busy."
|
|
at := { 0. 1 }. speed := 0.73. held := 0. score := 0.
|
|
tick := Time millisecondClockValue. step := 0.033. eyed := 0. steer := #pointer.
|
|
respawn := [
|
|
"Move it to the top before showing the next icon, or the
|
|
one just caught is seen changing face at the paddle."
|
|
at := { (-85 to: 85) atRandom / 100.0. 1.15 }.
|
|
faller ramp: 'at' from: at to: at over: 0.
|
|
faller image: 'icon' form: icons atRandom ].
|
|
playing := [ [ game isDown: #escape ] whileFalse: [
|
|
| wanted below landed was fell beat aimed chase nudge |
|
|
"How long the last turn round the loop actually took, which
|
|
is what the next move has to be spread over: a ramp given
|
|
less is cut off partway and the rest of the move arrives as
|
|
a jump, which is seen as a stutter."
|
|
beat := Time millisecondClockValue.
|
|
step := ((beat - tick max: 1) min: 200) / 1000.0.
|
|
tick := beat.
|
|
|
|
"Whichever was used last steers, and goes on steering. The
|
|
pointer answers where it last sat even when it is off the
|
|
scene or standing still, so following it unasked takes the
|
|
paddle out of the keys' hands the moment a key comes up."
|
|
aimed := game pointer first.
|
|
(game hasPointer and: [ (aimed - eyed) abs > 0.002 ])
|
|
ifTrue: [ steer := #pointer ].
|
|
eyed := aimed.
|
|
nudge := 0.
|
|
(game isDown: #left) ifTrue: [ nudge := nudge - (2.1 * step) ].
|
|
(game isDown: #right) ifTrue: [ nudge := nudge + (2.1 * step) ].
|
|
nudge = 0 ifFalse: [ steer := #keys ].
|
|
wanted := steer = #pointer
|
|
ifTrue: [ aimed ]
|
|
ifFalse: [ held + nudge ].
|
|
was := held.
|
|
"Catching up on a clock rather than per turn, so how closely
|
|
the paddle follows does not ride on how long a turn took."
|
|
chase := 1 - (0.08 raisedTo: step / 0.06).
|
|
held := ((held + ((wanted - held) * chase)) max: -0.95) min: 0.95.
|
|
|
|
below := at second - fallerHalf.
|
|
fell := at.
|
|
at := { at first. at second - (speed * step) }.
|
|
landed := below > paddleTop and: [ at second - fallerHalf <= paddleTop ].
|
|
|
|
"Ramps rather than steps: the loop runs at 30 a second and
|
|
the view draws at 60, so the renderer carries each move
|
|
the rest of the way on its own clock."
|
|
paddle ramp: 'at' from: { was. -0.8 } to: { held. -0.8 } over: step.
|
|
faller ramp: 'at' from: fell to: at over: step.
|
|
shadow
|
|
ramp: 'at' from: { fell first. -0.9 } to: { at first. -0.9 } over: step;
|
|
uniform: 'spread' value: (1.0 + (at second + 0.9) * 0.8 max: 0.35).
|
|
|
|
landed
|
|
ifTrue: [
|
|
(at first - held) abs < (paddleHalf + fallerHalf * 0.75)
|
|
ifTrue: [
|
|
score := score + 1.
|
|
speed := speed + 0.067.
|
|
paddle uniform: 'warmth' value: (score / 20.0 min: 0.85).
|
|
tally show: 'SCORE ', score printString.
|
|
(LumaTune named: #blip channel: 3)
|
|
patch: #pulse; tempo: 320; loops: false;
|
|
notes: { #b5. #e6 }; play.
|
|
respawn value ] ]
|
|
ifFalse: [
|
|
at second < -1.2 ifTrue: [
|
|
score := 0.
|
|
speed := 0.73.
|
|
paddle uniform: 'warmth' value: 0.
|
|
tally show: 'SCORE 0'.
|
|
(LumaTune named: #blip channel: 3)
|
|
patch: #bass; tempo: 260; loops: false;
|
|
notes: { #e2. #c2 }; play.
|
|
respawn value ] ].
|
|
|
|
(Delay forMilliseconds: 33) wait ].
|
|
LumaTune hush ] fork.
|
|
game
|
|
"""),
|
|
PharoExample(
|
|
title: "Lettering, drawn from an atlas",
|
|
code: """
|
|
"The glyphs are rasterised once into an atlas the shader
|
|
samples. Saying something else costs a buffer of corners --
|
|
no rasterising, no recompiling -- so it stands up to text
|
|
that changes every frame."
|
|
words := LumaCanvas new.
|
|
sign := LumaText on: words addDrawable pointSize: 56.
|
|
sign
|
|
pad: #(28 24) tint: #(0.2 0.95 0.75);
|
|
show: 'HELLO FROM THE GPU'.
|
|
words
|
|
"""),
|
|
PharoExample(
|
|
title: "Counting up, a fresh string every frame",
|
|
code: """
|
|
"Run the lettering example first. Nothing is rasterised here:
|
|
each new string is corners and nothing else."
|
|
ticking := [ 1 to: 600 do: [ :each |
|
|
sign show: 'FRAME ', each printString.
|
|
(Delay forMilliseconds: 16) wait ] ] fork
|
|
"""),
|
|
PharoExample(
|
|
title: "An effect over the whole view",
|
|
code: """
|
|
"Nothing special: a drawable whose two triangles cover the
|
|
view, so an effect sits among an object's views like the
|
|
rest of a scene."
|
|
backdropScene := LumaCanvas new.
|
|
backdrop := backdropScene addDrawable.
|
|
backdrop
|
|
attribute: 'p' components: 2;
|
|
varying: 'uv' components: 2;
|
|
vertexSource: 'void main() {
|
|
uv = p * 0.5 + 0.5;
|
|
gl_Position = vec4(p, 0.0, 1.0); }'
|
|
fragmentSource: 'void main() {
|
|
float d = length(uv - 0.5);
|
|
frag_color = vec4(0.5 + 0.5 * sin(u_time + d * 24.0), uv.x, uv.y, 1.0); }';
|
|
mesh: #(-1 -1 1 -1 -1 1 1 -1 1 1 -1 1) primitive: #triangles.
|
|
backdropScene
|
|
"""),
|
|
]),
|
|
("Sound", [
|
|
PharoExample(
|
|
title: "Lead over bass \u{2014} two channels at once",
|
|
code: """
|
|
LumaSynth start.
|
|
(LumaTune named: #lead channel: 0)
|
|
patch: #pulse; tempo: 132;
|
|
notes: #(0 4 7 4 0 4 7 11 7 4 0 - 0 4 7 -);
|
|
play.
|
|
(LumaTune named: #bass channel: 1)
|
|
patch: #bass; tempo: 132; division: 2;
|
|
notes: #(c2 - c2 - as1 - g1 -);
|
|
play
|
|
"""),
|
|
PharoExample(
|
|
title: "Retune the lead while it loops",
|
|
code: """
|
|
"Run the two-channel example first, then run this. The loop
|
|
keeps time; the new notes land on the next bar."
|
|
(LumaTune named: #lead)
|
|
scale: #(0 2 4 7 9);
|
|
notes: #(0 2 4 7 9 7 4 2 0 - 4 - 7 - 9 -);
|
|
tempo: 168
|
|
"""),
|
|
PharoExample(
|
|
title: "Noise percussion on its own channel",
|
|
code: """
|
|
LumaSynth start.
|
|
(LumaTune named: #drums channel: 2)
|
|
patch: #noiseHit;
|
|
tempo: 132;
|
|
notes: #(c5 - - - c5 - c5 -);
|
|
play
|
|
"""),
|
|
PharoExample(
|
|
title: "Pickup blip \u{2014} a one-shot rising pair",
|
|
code: """
|
|
LumaSynth start.
|
|
(LumaTune named: #sfx channel: 0)
|
|
patch: #pulse;
|
|
tempo: 300;
|
|
loops: false;
|
|
notes: #(b5 e6);
|
|
play
|
|
"""),
|
|
PharoExample(
|
|
title: "Jump \u{2014} a fast run reads as a sweep",
|
|
code: """
|
|
LumaSynth start.
|
|
(LumaTune named: #sfx channel: 0)
|
|
patch: #pulse;
|
|
tempo: 420;
|
|
loops: false;
|
|
notes: #(c4 e4 g4 c5 e5 g5 c6);
|
|
play
|
|
"""),
|
|
PharoExample(
|
|
title: "Acid line \u{2014} the filter is the instrument",
|
|
code: """
|
|
"Every note strikes the filter open and it shuts again on its
|
|
own. Resonance and a little drive do the rest."
|
|
LumaSynth start.
|
|
LumaSynth echo: 0.24 feedback: 0.35 mix: 0.3.
|
|
(LumaTune named: #acidline channel: 0)
|
|
patch: #acid; tempo: 300;
|
|
notes: #(a1 a1 c2 a1 d2 a1 g2 f2 a1 a1 c2 e2 a1 g1 f1 -);
|
|
play
|
|
"""),
|
|
PharoExample(
|
|
title: "Reedy lead, ringing bell, breathing pad",
|
|
code: """
|
|
"Three of the host's own patches, sounding together, and each
|
|
put somewhere of its own. Hush with: LumaTune hush"
|
|
LumaSynth start.
|
|
LumaSynth echo: 0.3 feedback: 0.4 mix: 0.35.
|
|
(LumaTune named: #lead channel: 0)
|
|
pan: -0.35;
|
|
patch: #pwmLead; tempo: 132;
|
|
notes: #(c5 - e5 - g5 e5 c5 - d5 - f5 - a5 g5 e5 -);
|
|
play.
|
|
(LumaTune named: #chime channel: 1)
|
|
pan: 0.55;
|
|
patch: #bell; tempo: 132; division: 4;
|
|
notes: #(c6 - - g5);
|
|
play.
|
|
(LumaTune named: #wash channel: 2)
|
|
pan: 0;
|
|
patch: #pad; tempo: 132; division: 8;
|
|
notes: #(c3 a2);
|
|
play
|
|
"""),
|
|
PharoExample(
|
|
title: "Colour a voice yourself",
|
|
code: """
|
|
"What a voice does beyond its envelope: the pulse it draws,
|
|
the slow wave moving it, what the filter does when struck,
|
|
and how driven it comes out. What is left out is left off."
|
|
LumaSynth start.
|
|
LumaSynth channel: 0 preset: #pwmLead.
|
|
LumaSynth channel: 0 colour: {
|
|
#pulseWidth -> 0.2.
|
|
#lfoRate -> 6.
|
|
#lfoToWidth -> 0.28.
|
|
#lfoToPitch -> 0.15.
|
|
#drive -> 0.25 } asDictionary.
|
|
(LumaTune named: #lead channel: 0)
|
|
tempo: 150; notes: #(c5 e5 g5 c6 g5 e5 c5 -);
|
|
play
|
|
"""),
|
|
PharoExample(
|
|
title: "Getting back",
|
|
code: """
|
|
"Stuck droning? This stops every tune and every voice with
|
|
it, and leaves what the channels are set to alone:
|
|
LumaSynth hush
|
|
Back to how the synth started -- patches, colours, places,
|
|
the echo and the level:
|
|
LumaSynth reset
|
|
Or just the channel that got away:
|
|
LumaSynth resetChannel: 0"
|
|
LumaSynth hush
|
|
"""),
|
|
PharoExample(
|
|
title: "Shape your own patch",
|
|
code: """
|
|
"Every field of a voice, as plain data. Channel 0, a hollow
|
|
detuned saw with a long resonant sweep."
|
|
LumaSynth start.
|
|
LumaSynth
|
|
channel: 0 waveform: LumaSynth saw detune: 0.12
|
|
attack: 0.01 decay: 0.5 sustain: 0 release: 0.1
|
|
cutoff: 1400 resonance: 0.7 gain: 0.6.
|
|
(LumaTune named: #lead channel: 0) tempo: 96; notes: #(0 - 5 - 7 - 12 -); play
|
|
"""),
|
|
PharoExample(
|
|
title: "F\u{00FC}r Elise \u{2014} melody over chords",
|
|
code: """
|
|
"Beethoven, 1810, long out of copyright. The right hand runs in
|
|
sixteenths on one channel; the left hand's chords land on
|
|
another, three pitches to a step."
|
|
LumaSynth start.
|
|
(LumaTune named: #elise channel: 0)
|
|
patch: #blip; tempo: 120;
|
|
notes: #(e5 ds5 e5 ds5 e5 b4 d5 c5 a4 - - -
|
|
c4 e4 a4 b4 - - - e4 gs4 b4 c5 - - -);
|
|
play.
|
|
(LumaTune named: #eliseChords channel: 1)
|
|
patch: #blip; tempo: 120;
|
|
notes: #(- - - - - - - - #(a2 e3 a3) - - -
|
|
- - - - #(e2 e3 gs3) - - - #(a2 e3 a3) - - -);
|
|
play
|
|
"""),
|
|
PharoExample(
|
|
title: "A chord every step",
|
|
code: """
|
|
"Up to four pitches share a step. Nest them and they sound
|
|
together."
|
|
LumaSynth start.
|
|
(LumaTune named: #pad channel: 3)
|
|
patch: #blip; tempo: 76;
|
|
notes: #(#(c3 e3 g3) - #(a2 c3 e3) - #(f2 a2 c3) - #(g2 b2 d3) -);
|
|
play
|
|
"""),
|
|
PharoExample(
|
|
title: "Silence everything",
|
|
code: "LumaTune hush."),
|
|
]),
|
|
("Decode", [
|
|
PharoExample(
|
|
title: "The pattern library",
|
|
code: "LumaProject patterns."),
|
|
PharoExample(
|
|
title: "Decode bytes with a pattern",
|
|
code: """
|
|
(LumaProject patterns at: 'macho.hexpat')
|
|
decode: 'MachHeader'
|
|
from: #[16rCF 16rFA 16rED 16rFE 16r0C 16r00 16r00 16r01 16r00 16r00 16r00 16r00 16r02 16r00 16r00 16r00 16r11 16r00 16r00 16r00 16r20 16r04 16r00 16r00 16r85 16r00 16r20 16r00 16r00 16r00 16r00 16r00]
|
|
at: 16r100000000
|
|
"""),
|
|
PharoExample(
|
|
title: "Decode a session's main module header",
|
|
code: """
|
|
| session |
|
|
session := LumaProject sessions items first.
|
|
(LumaProject patterns at: 'macho.hexpat') decodeIn: session at: 16r100000000
|
|
"""),
|
|
]),
|
|
("Explore", [
|
|
PharoExample(
|
|
title: "Filter a big list (type in the field)",
|
|
code: "Smalltalk globals keys asSortedCollection asArray."),
|
|
PharoExample(
|
|
title: "Browse — put the cursor on a message, press \u{2318}M / \u{2318}N",
|
|
code: "OrderedCollection new addAll: #( 1 2 3 ); yourself."),
|
|
PharoExample(
|
|
title: "Format — messy code, press \u{2318}\u{21E7}F",
|
|
code: "x:=1+2*3. y:=x factorial. { x. y }."),
|
|
]),
|
|
("Basics", [
|
|
PharoExample(
|
|
title: "A collection to inspect",
|
|
code: "1 to: 100."),
|
|
PharoExample(
|
|
title: "A shared variable",
|
|
code: "total := (1 to: 10) inject: 0 into: [ :a :b | a + b ]."),
|
|
]),
|
|
]
|
|
}
|