Skip to content

Ollin 0.13.0

Latest

Choose a tag to compare

@eaviles eaviles released this 02 Oct 05:11
· 33 commits to main since this release
0.13.0
d52eb8d

Changed

  • The inspector's number boxes step. Holding Shift while dragging a value box used to move it ten times faster, which ran it to an end of its range in about 25 points; Shift now keeps the plain speed and lands on round marks (every 10 in a box from 10 to 375, every 0.1 from 0 to 1). A whole number drags at a speed that follows its range, so a count over 0 to 10,000 crosses it in the drag a slider takes instead of in thousands of points. While a box is being typed in, the up and down arrows step it to its next mark, with Shift and Option for bigger and smaller steps, starting from what has been typed. A whole number's minus and plus buttons repeat while held, and the line a save leaves under the button goes once a value is turned again. Parameters

  • A color outside sRGB reads the components the system gives it. Color(displayP3:green:blue:) stores a color as the sRGB components that mean the same color, and a component below zero went through the transfer curve's linear segment, so pure Display P3 red read (1.093, -0.544, -0.254). The curve is now mirrored about zero, the extended-range convention NSColorSpace.extendedSRGB uses, and the same red reads (1.093, -0.227, -0.150); displayP3Components still comes back to (1, 0, 0), and Color(linear:) and linearRGB round-trip on either side of zero. Only a component below zero changes, so a color inside sRGB is untouched, and a picture that names a color outside it and reads its components back sees the new numbers. Color output

  • A generated project says sketch. ollin new's sample name is MySketch (it was MyPiece); a widget project's one public line is Door.swift, declaring <Name>Door (it was Piece.swift and <Name>Piece), and a project already generated keeps its own names; the generator's menu-bar kind is titled Menu bar strip; and the installation supervisor's log lines say "the sketch" where they said "the piece". Project generator, Widget, Installation

  • Noise along one coordinate is a field of its own. noise(x), signedNoise(x), fbm(x), ridgedFbm(x) and turbulence(x) read the three-coordinate lattice along its x axis, where six of the sixteen gradient directions have no x component, so the curve left three eighths of its lattice points with no slope and ran dead flat across a whole cell wherever two such points met (a handheld camera's drift paused on them). They now read a one-dimensional gradient field seeded the same way, so the curve is never flat between two lattice points, and noiseField() and a formula's noise at y 0 and z 0 read the same field. The values change: a sketch reading one-coordinate noise gets a different curve of the same character under the same seed, and the two- and three-coordinate forms are untouched. Noise

  • The extension starter's filter is cornerShade(amount:). ollin new --kind extension --seam filter wrote a Filter.vignette(amount:) beside the built-in vignette(amount:radius:softness:), so the starter's own .vignette() call was ambiguous. It now writes cornerShade(amount:), called as .cornerShade(); a package made from the old starter can rename its function the same way. Extensions

  • The Guide is forty-five chapters. Four chapters split along their families: Automata from Simulations on a grid, Materials and surroundings from Meshes, maps, and materials (now Meshes and maps), The iPhone as a sensor from Depth and the iPhone as a sensor (now Depth), and Making it physical from Finishing a sketch. Every chapter from 24 on has a new number, so a link to a chapter file needs the new one: 24 is now 25, 25 and 26 are 26 and 27, 27 to 33 are 29 to 35, 34 to 38 are 37 to 41, and 39 to 41 are 43 to 45. One file has a new name as well as a new number, 35-Depth.md (it was 33-DepthAndThePhone.md), and the new chapters are 24-Automata.md, 28-MaterialsAndSurroundings.md, 36-ThePhoneAsASensor.md, and 42-MakingItPhysical.md. A link to a section that moved names its new chapter. The Guide's contents list them all.

  • The Guide's finished sketches compose more of their chapters. Chapter 36 ends on a dancer read from the film that ships with Ollin and drawn in lit solids. Chapter 40's music box swings, follows a chord progression, and writes what it played as a MIDI file. Chapter 43's live-coded set saves looks as cues and calls one back. In eight chapters, the sections a finished sketch does not use now come after it, as families. Guide

  • The credits carry the history the Guide tells. ATTRIBUTION.md gains an entry for each chapter, naming the people, works, and dates behind its ideas with a source to read, and several chapters correct a date or a name against those sources. Attribution

  • The headless calls that hand back a picture throw an ExportError, where they answered nil and printed a line. When the renderer would not start, most often because a shader stopped compiling, OllinApp.image(of:) returned nil and the reason went to the console, so code that rendered a frame could not tell a broken shader from a missing GPU. It now throws the same ExportError the exports throw, of kind unrendered, with the renderer's own message in its sentence and an empty path. Breaking: image(of:), the four contactSheet(of:) forms, plates(of:), and separations(of:) now throw and return a value that is no longer optional. A sweep over a parameter the sketch does not declare, an empty seed or value list, and a separation with no inks or no printing condition throw an unsupported one naming what to add, where they returned nil. In code, if let frame = OllinApp.image(of: sketch) becomes let frame = try OllinApp.image(of: sketch), or try? to keep the old answer. exportWidget throws when no picture draws, where it wrote an empty folder. Export

Added

  • The pitch wheel as a parameter. MIDIInput.bindPitchBend(to:channel:travel:) drives a @Param from the wheel the way bind(controlChange:to:) drives one from a knob: at rest the parameter sits exactly at the middle of its range, and each end of the wheel reaches that end. travel is on the wheel's own scale, -1 fully down to 1 fully up, so travel: 0...1 spends the whole range on an upward bend. pitchBend(channel:) reads the wheel on the same scale, 0 until it moves, and unbindPitchBend(channel:) lets go. This is the whole channel's wheel; on a per-note controller each note's bend is still in heldNotes. MIDI

  • A still sketch that answers, and knowing an export. redraw() draws one more frame of a sketch that has stopped looping, so a noLoop() sketch can change what it shows on a click or a key and cost nothing between them. isExporting is true for the whole of any export (setup(), the frames run up to the first one written, and every written frame) and false in a window and under --bench, so a sketch that spreads slow work over live frames can do all of it at once when every frame is written. Sketch

  • The pointer, the open panel, and the clipboard. pointerShape(_:) gives the pointer over the canvas one of the system's shapes (PointerShape: crosshair, open and closed hands, and nine more), and hidePointer() / showPointer() take it away and bring it back, over the canvas only and apart from an installation's own hiding. chooseFiles(withExtensions:allowsMultiple:) shows the open panel without stopping the frames, and the pick arrives as String paths through chosenFiles() and the filesChosen() hook, the way a drop does. copyFrame() puts the frame the window shows on the clipboard as a PNG. Input, Recording

  • Spherical coordinates. spherical(azimuth, elevation, radius, around:) is polar in 3D, the angles first, in the camera's own orbit convention, and a Vector3's azimuth and elevation read the angles back. Math

  • Points inside a shape. randomPoints(in:count:) scatters points evenly over a Shape's fill, holes left out and every island given its share by area, drawn from the same triangles the fill is made of rather than thrown at the box around the shape; randomPoints(along:count:) scatters them along the outline, even by walked length; and poissonDisk(in:radius:) is the blue-noise form kept inside the shape, seeded from the fill's own triangles and restarted from a fresh seed when the darts run out, so a thin shape fills end to end and every island fills. All three run on the sketch's seeded random, and each wraps a free function that takes any random source. (Geometry, Blue noise)

  • One mesh from several. Mesh.joined(_:) lays parts end to end as one mesh with no boolean, and placed(_:) bakes a MeshInstance placement into a part first (positions by the matrix, normals by its inverse transpose, tangents with the surface, a tint into the vertex colors). The join runs the arithmetic drawMesh runs under the transform stack, so the result renders as its parts would drawn one by one, in one draw call; union(_:) stays the answer for a watertight print. (3D)

  • The metaball curve on its own. Metaballs.falloff(distanceSquared:radius:) is the bump one ball is made of, in a sketch's terms (1 at the center, a half at the radius, 0 from twice the radius out), and Metaballs.Ball.value(at:) is one ball's own contribution, so a sketch shapes its own blobs, a rim that wobbles, a ball stretched along an axis, from the curve instead of a copy of its polynomial. (Isosurfaces)

  • Three more filters. .channelMixer(_:) runs a ColorMatrix over a layer, every channel of the result a weighted sum of the input's red, green, blue, and alpha plus an offset: .gray and .gray(red:green:blue:) for a monochrome mixed to a recipe, .swapping(.red, .blue) to trade two channels, or a matrix built row by row. .lensDistortion(amount:quartic:center:fillsFrame:) bends a layer into a barrel or a pincushion by the Brown-Conrady radial model, leaves what read past the edge transparent, and with fillsFrame scales the read so no border shows. .cornerPin(topLeft:topRight:bottomRight:bottomLeft:) lays a layer onto four points through the same map installation mode's projection runs, each corner defaulting to its own place so moving one is one argument. The identity of each changes no byte, and all three cross to the web page. Effects

  • Motion for a mesh that changes shape. drawMesh(_:previous:) takes last frame's vertex positions beside the mesh, so a ribbon rebuilt every frame, a marching-cubes surface, or a cloth is followed vertex by vertex: temporalAntialiasing() keeps its edges refined mid-motion and motionBlur() streaks it where it bent, not only where the camera moved. withMotion { } still covers a mesh that moves whole; inside such a block the previous positions use last frame's placement too, so a mesh that both moves and bends writes the sum. A count that does not match the mesh notes once and draws with transform-only motion. 3D

  • A surface that glows in its own colors. MeshMaterial.emissiveIntensity, set on a copy by Mesh.glowing(_:), adds the surface's own color (the fill, the base color, the vertex colors, and the texture) as light of its own, so a vertex-colored mesh glows in each vertex's hue where emissiveColor tints the whole mesh one color. The two add. It works with no lights, traces in a path-traced export, and a spatial export writes it into the material's one emissive slot as the base color times the intensity. 3D

  • The environments on a menu. Environment conforms to ParamChoices, so @Param var surroundings = Environment.studio gives the inspector a pop-up menu of the twenty built-in environments. A .sky(...) or a built-in with lightingOnly() applied is off the menu and reads as the first entry; keep the parameter on the plain built-in and apply the modifier where the sketch uses it. 3D

  • A radius per corner. drawRect(_:cornerRadii:), on every anchor cornerRadius: takes and on drawRects, rounds each corner of a rectangle by its own radius: CornerRadii.top(20) for a tab, .bottom, .left, and .right for the other pairs, or all four named. Two radii that add up past their side scale down together so the shape is kept. Four equal radii draw exactly as one radius does. The dashed outline, the hollow band, and the merged fields (SDF.rect(width:height:cornerRadii:), SDF.square(_:cornerRadii:)) carry the four, and the SVG and PDF exports write the outline as a path with an arc at each rounded corner. Drawing

  • A gradient that sweeps around a point. Gradient.conic(center:startAngle:) lays a ramp once around a center from an angle the sketch chooses (radians from three o'clock, clockwise, like every angle in Ollin), for a color wheel, a dial, or a pie. It paints fills and strokes on every shape, the tessellated shapes and outline text by vertex, and the merged fields by position, and it crosses to the web page. On the tessellated path a fill triangle that straddled the sweep's seam used to shade backwards through the whole ramp; it is now cut along the seam, which also mends an along-path fill. The vector exports, which have no sweep, write the ramp's midpoint color as they do for an along-path fill. Color

  • A generator's plane as a value. ComplexPlane is the framing every picture of the complex plane shares (center in the middle, span units across the shorter side, the imaginary axis up), and a generator's plane hands its own back, so plane.canvasPoint(of: c, in: bounds) marks a number on the picture and plane.planePoint(at: mouse, in: bounds) reads the number under the pointer without the sketch redoing the arithmetic. ComplexPlane(center:zoom:) takes a generator's zoom, where the span is 3 / zoom. Effects

  • Putting a parameter back. A parameter tuned in the inspector goes back to the value its @Param line declares without quitting the host: a dot after the name marks a tuned row and puts it back on a click, Reset is in every row's menu, a group card offers Reset for its rows, and Reset all sits beside the save button. The sketch keeps running, with no reload and no setup(), so the clock, the seed, and the canvas carry on, and a track on the timeline still wins its parameter. From code, Param.reset() and Sketch.resetParameters(named:), and declaredValue reads what the line gave the parameter; the remote surface has a Reset all of its own (RemoteReset on the wire). A host that shows the parameters list passes a ParamResetAction as its new reset: argument (ParametersListView(parameters:sketchName:onChange:save:reset:)), which carries the tuned names and the call that puts values back. Parameters

  • Alma Thomas joins the recreations. Examples/Recreations/AlmaThomas is new, after the paintings of 1966 to 1972 made of one stroke of a flat brush laid down and lifted, the next a little way on, the canvas showing between them. Rings lays the strokes around a core in rings, each color a dealt number of rows and the last running out to the corners, after Resurrection and The Eclipse; Columns lays them down ruled columns, in a disc of red and orange with a seam of yellow, in bands of one color after another, red on a dark ground, or blues with a patch of warm strokes at one corner, after Snoopy Sees Earth Wrapped in Sunset, Earth Sermon, Mars Dust and Starry Night and the Astronauts. Both are built on strokeBrush: a ring or a column is one path, the stroke is a block-shaped tip stamped along it, and the ground between strokes is the spacing, so --export-svg gives every stroke back as its own outline centered on its circle or column. Each is painted stroke by stroke over the first two thirds of its cycle, holds, and is taken back the way it came.

Fixed

  • A stack of contour levels costs one march. isolines(at: levels, ...) sampled the field once but marched every cell again for each level, about 0.8 ms a level on a 650-wide grid, so a contour map of a cheap field paid for every level in full. Each cell now visits only the levels that pass between its corners, and joining the crossings into contours is faster too: 28 levels of a smooth lookup went from 22 ms to 8.5 ms beside 2.4 ms of sampling, with the same contours. Isolines

  • A hole in a contoured field leaves the contour open. A field that returned NaN or infinity next to a contour made isolines interpolate a crossing onto NaN and stop the program. A cell with a corner that is not finite now holds no contour, so the line ends at the hole's edge. Isolines

  • Input after a live reload reaches the sketch that is drawing. In OllinLive and OllinLiveCoding, a click, a key, a drop, or the pointer's position after an edit went to the first instance the window mounted rather than the one swapped in, so a mouse-driven sketch stopped answering after its first reload. The canvas now follows every swap.

  • A simulation's substeps cost what their kernels cost. Every pass of a Sim.shader kernel, and every user shader, composed its full Metal source (the shared library spliced in, a few hundred kilobytes) and hashed it to find its compiled pipeline, on every encode: 17 ms a substep in a debug build, so a 25-substep field ran at two frames a second with the GPU idle, and --bench showed the time as end-to-end rather than GPU. The hash is now remembered by what the shader itself says (its text, its file, its modules, the variant and the output format), so a pass costs its draw; the same 25-substep field exports 60 frames in 2.5 s where it took 37. Effects

  • A diffusion curve holds its far side. Past the band of a two-sided drawDiffusionCurve, the region held by one side alone came back as the mean of both sides (a navy sea under a dusk sky rendered rust brown), at any sharpness. The solve ran a coarse-to-fine cascade of relaxation passes, which settles a picture's broad shape but, at every level where the curve's two sides share a texel, holds their mean as a wall and lets it leak across. .diffuse now runs multigrid V-cycles over that first guess: each level measures what it still gets wrong, the level below solves for the correction with the marks held as walls, and the correction is carried back up. Each side fills its region, and sharpness buys cycles, three at 0 and six at 1. Effects

  • A transparent pixel of a mark writes no depth. Under depth(at:), a circle, a rect, a glyph, an image, and a point-cloud splat wrote depth over their whole quad, the transparent corners and halo included, so a mark set behind another cut a straight edge out of it where nothing was drawn. A pixel with no coverage is now discarded before the depth write, so a mark hides only where its ink is. Depth compositing

  • A neighbor grid covers its bounds. SpatialHash, and with it Particle Life, the Primordial Particle System, Particle Lenia, swarm chemistry, Swarm, ParticleFluid and SoftBodies, sized its world as a whole number of cells of the radius, so a radius that did not divide the bounds left the far edge outside the world: at a radius of 34 in a 253-wide panel the particles wrapped at 238, leaving a 15-pixel strip empty along the right and the bottom. The cells now stretch to cover the bounds exactly, each at least the radius wide so a search still reaches every neighbor, and the world is the bounds. Compute

  • A kite-and-dart tiling covers its patch. Penrose.tiles(.kitesAndDarts, in:tileEdge:) split each half-kite into a piece that was no half-kite, so a third of the halves found no mirror partner and were dropped, and the tiling came back with bare patches through it (about 31% of a panel) and too few kites. The split is the classic one now, two half-kites and a half-dart, each tiling covers its patch once, the kites outnumber the darts by the golden ratio, and the arcs meet across every edge; a test that samples the patch's interior pins the coverage the edge test could not see. The rhombs were right all along. Aperiodic tilings

  • A physics snapshot keeps a body's own mass and center of mass. A body given a mass or a center of mass rather than one worked out from its collider, as a scene's physics:mass and physics:centerOfMass give them through the import, came back from restore(_:) at the mass its shape and density make, balanced at its middle. The snapshot now writes both, in format 6, and reads formats 4 and 5 as before. Physics3D

  • A small chance in a simulation is the chance it says. The forest fire's lightning and growth, the Ising model's flips, and a Schelling agent's move compared a probability against a float hash that lands on exactly 0 about once in 1,800 rolls, so any chance below about 5e-4 fired at that floor instead. The forest fire's default lightning struck about 90 times too often, so the default ratio of growth to lightning was near 28 to 1 rather than 2,500 to 1, and a magnet below its critical temperature kept flipping spins it should have held. The rolls now draw on steps of 2^-24, so a lightning of 1e-4 lights 1e-4 of the forest and a cold magnet holds still. A run with these sims under a seed draws a different run from before. Effects

  • The bundled Hershey font's notice carries the acknowledgments its distribution requires. The .jhf data is used under terms that ask two credits to travel with it, the original author's and the format's author's, and the notices called it public domain with no requirement. Both now appear as worded, in THIRD-PARTY-NOTICES.md and in the notice beside the font. Notices

  • --cue finds a look saved beside a live sketch. An OllinLive export given --cue <name> and no --cues looked for the cue in a sheet its export path never loaded, so a look saved from the inspector was reported missing. The export path now loads Sketch.cues.json from beside the sketch, as it loads the automation, and a --cues file still wins. Cues

  • A forest fire takes the trees you plant. Drawing into a .forestFire field blended the mark in by its coverage, in linear light, so the mid-gray the reference names (Color(white: 0.5)) arrived a third of the way to a tree and planted nothing, and a rect's soft rim spilled trees into the cells beside it. A mark now snaps to the nearest of the three states in the gray the sketch wrote, and a rim under three-quarters coverage writes nothing, as Wireworld, Schelling's board, and the Ising model already did. A gray that planted trees before, from about 0.54 to 0.74, still does. Effects

  • Caustics hold still in an exported video. An export traced a new set of photons at every frame and keeps no history to average them, so a slowly moving scene flickered where the caustic light fell, about 5% up and down from frame to frame, with the sparks changing shape each frame. An export now traces the same photons at every frame, so the caustic moves only as the glass and the light move. The live window is unchanged. A still export's caustic comes out a little different from before, since it was drawn with that frame's photons. Caustics

  • Glass shows the scene behind it at the brightness that scene has. With sceneThroughGlass() on, the view through a transmissive surface was scaled by the environment's intensity and by the environment's own exposure, so clear glass over a lit floor passed about a fifth of the floor's light under an environment at 0.4 and half at 1. It now passes all but what its surface reflects away, about 96% for glass of index 1.5, whatever the environment. Sketches that use sceneThroughGlass() without ray tracing render brighter through their glass.

  • Atlas text draws every glyph right on the frame that fills the font's atlas page. The page was cleared and refilled in the middle of that frame, so the glyphs drawn earlier in it pointed into a page that now held other glyphs, and they showed as the wrong characters for that one frame, again each time a long run of text in many scripts filled a page. A full page is now kept as it is and the atlas moves on to a fresh one, so text drawn before, a retained Batch that holds atlas text, and a web recording all keep the glyphs they were drawn with. A web export no longer stops when a font fills its page during the recording; each page crosses as its own picture.

  • surfacePoints with the default .blueNoise scatter is about three times faster and returns the same points. Its thinning pass pushed a sample's weight into its queue a second time each time it passed over an outdated entry, so the queue filled with copies. Four hundred thousand points on a terrain took over ten minutes in a debug build, and now take three and a half.

  • The Mandelbrot, Julia, and orbit-trap generators frame the plane with the imaginary axis up, as domain coloring, Newton's basins, the shader library's complexPlane, and the reference say; they read it down, and nothing said so. A Julia set with c off the real axis now draws mirrored left to right from before, and a Mandelbrot centered off the real axis top to bottom; the Guide's marked-c figure had marked the conjugate point.

  • ollin examples describes every sketch in its own words: the picture grid at the top of a listing had renamed the first sketch of each grid row after its neighbors (ImaginaryLog read as "Meromorphic | ShaderBlend | ShaderFilter"). A search lists the sketches whose description matches beside the ones whose name does, where a name match used to hide them (complex now finds Meromorphic as well as ComplexPlane), and a sketch is found by the author its header credits (turan finds the three written after that article); the single-sketch view prints the credit.

  • Command-Z and Shift-Command-Z no longer reach a sketch's keyPressed() when the host has nothing to undo.

  • TUIOReceiver reads a real tracker's frame as one frame. TUIO sends each frame's alive list first, then the sets, then the frame number, and the receiver closed a frame on the first set after an alive list, so every frame from a tracker was taken twice: framesReceived counted double, a lifted finger could leave before the moves of its own frame arrived, and a late datagram's alive list got past the frame-number check, which could blink a finger out. A frame now closes on its frame number, or on the next alive list for a tracker that sends none, and a frame that sends its sets first is still read.

  • The MIDIFiles example starts each bar's melody on the bar's downbeat. It restarted the arpeggiator on the downbeat itself, and a reset counts the step at its own position as played, so every bar's figure began an eighth late and the file ran an eighth past its eight bars. It now resets to the step before and reads each bar up to its last eighth.

  • An image the GPU holds, such as a layer's image or a compute texture's, reads .clear at every pixel and ignores a write, as its documentation says. Its pixel subscript treated it as a picture in memory. A read set aside a buffer the size of the image, and a write was kept there and read back but never drawn.