Skip to content

Ollin 0.15.0

Choose a tag to compare

@eaviles eaviles released this 12 Oct 14:33
· 113 commits to main since this release
0.15.0
650767b

Added

  • Segments, arcs, and ellipses as values. LineSegment, Arc, and Ellipse join Circle and Rectangle as values a sketch holds: each knows its length or perimeter (the ellipse's exact, through the arithmetic-geometric mean, where the usual formula is a few parts in ten thousand off on a flat one), its area, its bounds, the point on it nearest to any other, and its outline, and each is drawn by the call that always drew it (drawLine(_:), drawArc(_:mode:), drawEllipse(_:), which turns a tilted ellipse for you). All of them, with Circle, Rectangle, Triangle, and Contour, share the PlaneCurve protocol, so any two can be asked where they cross: crossings(with:) returns the meeting points sorted along the shape you asked, from the textbook closed forms where one exists (a straight piece against a circle or an ellipse, two circles, arcs keeping only their sweep) and, for an ellipse against anything round, from the two outlines polished onto the true curves. A mixed list of them can be asked pair by pair. Circle gains area, perimeter, point(at:), nearestPoint(to:), and distance(to:); Rectangle gains area, perimeter, and edges; Triangle gains perimeter and edges. New example: Examples/Shapes/Crossings. Where curves cross

  • A turtle you drive from code. Turtle is a pen that stands on the canvas facing some way: forward, back, left, and right (in degrees) walk and turn it, penUp and penDown break the drawing into runs, pushState and popState remember where it stood and take it back there without drawing the way, which is how a branch is drawn, and move(to:), home(), and a settable heading place it outright. Every run comes back as an ordinary open Contour, or a closed one after close(), so the walk strokes, fills, and plots like any outline. The L-system turtle reads a string; this one takes calls. New example: Examples/Patterns/TurtleWalk. Turtle

  • Anwar Jalal Shemza joins the recreations. Examples/Recreations/AnwarJalalShemza is new, after the painter from Lahore who settled in Stafford and built his pictures, he said, from two simple forms, the square and the circle, taken from carpets, Urdu calligraphy, and the architecture of Mughal Lahore. Meem is the letter he painted again and again from 1964 to 1968, reduced to a disc for its head and a line for its tail: the canvas is ruled into lanes, a cluster is a few heads laid one touching the next, and every head drops a tail on the cluster's outer side, the tails one gap apart, each leaving its head as a quarter turn around it and falling to the bottom edge, the top head's turn hooking over its shoulder. The picture can be mirrored, and the look is either the oils' thin red lines over blue and green bands or the stained-glass window's wide green stems with white outlines. The letters are written right to left as the pen would write them. Roots is his last series, 1977 to 1985: pieces of dyed cloth with frayed edges on a board, a flower on the top piece built by joining a half-disc and three discs, and on the pieces below its roots as a word in Urdu, set from a real face's own outlines in naskh, nastaliq, or Kufi, at a size taller than the cloth so it is cut where the cloth ends. Both export SVG that gives the rule back, and the words come back as the face's glyphs point for point.

  • Lofting. Mesh.loft(_:along:at:smoothed:scale:twist:capped:) and drawLoft build one surface through several flat shapes placed along a 3D path, so a bottle runs from a square foot through a round belly to a narrow neck, a horn rounds from a star into a circle as it turns, and a boat hull is lofted as a sheet through open ribs. Each section lands exactly where it was asked for, corners and all, between the path's own points or on them; the outlines are matched round their lengths with each seam turned to the nearest start on the section before, so the skin does not twist on its own; a corner sharp on any section keeps its edge down the loft and fades where the other sections round it off; three or more sections blend as a smooth curve, or straight from rib to rib with smoothed: false; and a loft of one shape is that shape's sweep, byte for byte. A sweep's and a loft's caps now meet the walls point for point (the cap's corners are the shape's own points rather than the triangulator's single-precision copies, and a run of points along one straight side no longer fills with slivers of no area), and both say whether they are closed, which the canvas depth of field reads. Lofting

  • A shader as paint. fill(shader) and stroke(shader) take a Shader anywhere they take a color, and its shade(uv, info) runs at every pixel the shape covers, on every shape and every path: the analytic shapes, polygons and Shapes, strokes, curves, and outline text. uv runs across the shape's own bounding box and turns with it under a transform; info.position is the pixel in canvas points, info.distance the signed distance to the shape's edge (negative inside, zero on a stroke's centerline), and info.along the fraction along a stroke. Coverage is applied after the shader returns, so the edge stays as sharp as a flat fill's, and a shader that returns one color draws what that color draws, byte for byte. A mesh under fill(shader) takes the shader as its surface texture, read at its uvs, so it reaches reflections, the tracer, and an export. Shapes painted by one shader draw as one run. A vector export writes a shader-painted shape in middle gray; a web page export stops and names the call. Paint.shader(_:) carries one as a value. New example: Examples/Shaders/ShaderPaint. A shader as paint

  • Hidden-line solids. featureEdges(creaseAngle:) draws a mesh the way a draughtsman draws a solid: the faces in the fill as always, and over them only the lines that describe the form, in the stroke at strokeWeight. Those are the creases where two faces meet past the angle, the boundary where a surface ends, and the silhouette, worked out for every copy from where it is seen. A box gives its twelve edges and no diagonals, a sphere its outline, and near solids hide the edges of far ones. The weight is in canvas points, a hairline on every copy at any distance, or in world units under strokeWeight(_:in: .world). Under drawMesh(_:instances:) every copy's edges are one more GPU draw placed by the same matrices, so ten thousand boxes cost what their faces cost plus a few milliseconds of GPU time and no CPU time; a mesh's edges are found once and kept while it is drawn. noFeatureEdges() turns it off. New example: Examples/3D/Geometry/HiddenLines. Hidden-line solids

  • Noise in three dimensions that loops. noise(x, y, z, loop:), with signedNoise, fbm, signedFbm, ridgedFbm, and turbulence in the same form, reads a volume that changes over a lap and comes back to exactly where it started, so a point cloud's drift or a cloud's density closes a seamless export. It reads a five-axis field, three for the point and two for the circle the lap tours, scaled to spread like noise(x, y, z) so a sketch keeps its contrast when it moves to the looping form. Noise

  • Live reload for a sketch that is a package. The live window compiles a sketch inside a SwiftPM target with every other .swift file of that target, so helpers can live in files of their own and a save to any of them reloads the window, where before a helper moved into a second file stopped the live route with cannot find ... in scope. ollin MySketch runs a package's sketch by its folder, a bare ollin runs the package you are in, and --target <name> picks one sketch of several. The compile reads Package.swift for the target's language mode, defines, features, and resources, so a picture in a processed Images/ folder loads through .module in the live window as it does under swift run. A sketch that imports another target of its package, or another package, still needs swift run, and the error says so. OllinRun (the --installation host) and OllinLiveCoding take a package's folder too. Single-file sketches

  • Hiroshi Kawano joins the recreations. Examples/Recreations/HiroshiKawano is new, after the philosopher who taught himself to program in 1963 to test a theory of aesthetics, and published some of the first computer-made pictures anywhere in 1964. Design walks a grid of black, red, blue, yellow, and white with a Markov chain of order 2, each cell chosen from what followed the last two in counts measured cell by cell off his Design 3-1 and Design 3-2; the printer types a code for every cell and the gouache goes on in the order the chain decided. Read in rows, the exported squares give back the counts they were walked with. ArtificialMondrian is the series of 1969: a chain sets where the lines run, a second decides whether each line runs on through a crossing, and the regions the black closes are colored at random, in three looks read from KD 55, KD 27, and KD 52. Every band, notch, and region exports as rectangles on the raster.

  • Smoothing that keeps the points. smoothed(neighbors:) on a Contour or a Shape is a moving average: each point moves to the average of itself and that many neighbors on either side, and the outline comes back with one point for each point it had, in the same order. So a traced or resampled ring loses its facets and its jitter and stays in step with anything keyed to its points, a color per point or a partner on another ring, where corner cutting doubles the count on every pass. A bell weighs the window by default, so near neighbors count most and a curve shrinks less; weights: .box is the plain average. A closed contour wraps around its seam, and an open one keeps its two ends exactly. Smoothing that keeps the points

  • Motion blur drawn from subframes. --subframes N draws each exported frame at N moments while the shutter is open and averages them in linear light, the accumulation buffer. It blurs anything a sketch draws, 2D and shaders included, at N times the render cost, and it is the reference the canvas motion blur is now measured against. --shutter F sets the opening, and left out it is the sketch's own motionBlur(shutter:), else 0.5. Every moment is a draw: time steps through the opening, deltaTime is the time since the last draw, and frameCount counts each moment, so motion counted in frames runs N times as fast. A picture the GPU carries from one draw to the next (a noClear pile, a feedback layer, an accumulator, a simulation field) is refused by name. In code it is OllinApp.exportSubframes and OllinApp.exportShutter. See Export.

  • Volumes. A Volume is a box of density samples, built from a function of position, from a list, or from noise filled across every core (Volume.noise, with a looping form for a cloud that comes home each lap). drawVolume(_:size:medium:) draws it in a 3D scene as smoke, cloud, fire, or ink, and the Medium says what it is made of: density (how thickly it blocks light, per world unit, like fog), color (the share of light it scatters), glow and glowIntensity (its own light), and anisotropy, with presets .smoke, .cloud, .fire, .ink, and .nebula. Each pixel's ray is marched through the box after the frame's solids are drawn and ends at the first one, so a solid inside a volume is veiled by the medium in front of it whatever order the calls came in. The medium scatters the scene's directional, point, and spot lights and its ambient and environment light, shades itself, and takes the shadows solids throw into it. Measured against the closed forms: transmittance by Beer's law, the glow's integral, and single scattering, each within half-float precision. A web page export names drawVolume and stops. The 3D/Effects/Volumes example sends a column of smoke up past a hanging ball. See Volumes.

  • Layer styles. Six filters give a layer an edge of its own, read from its alpha: .outline(width:color:align:) outside, inside, or centered on the edge; .dropShadow(offset:radius:color:) and .innerShadow(offset:radius:color:), the layer's alpha blurred and moved, laid under it or held inside it; .outerGlow(radius:color:) and .innerGlow(radius:color:), which follow the measured distance from the edge rather than a blur, so a thin line keeps its glow; and .bevel(width:depth:profile:angle:elevation:highlight:shadow:), the band in from the edge raised into a rounded or chiseled slope and lit. They chain like any filter, and the chain's order is the order they stack in. A clear color, or a width or radius of 0, hands back the layer's own bytes. Measured against the shape they were drawn from, an outline is within 0.06 pixel of its width on either side, and a drop shadow is exactly the layer's Gaussian moved. All six cross to a web page, and so does .distanceField, which used to stop the export. The Effects/LayerStyles example stacks them on a word under a moving light. See Layer styles.

  • Zdeněk Sýkora joins the recreations. Examples/Recreations/ZdenekSykora is new, after the Czech painter who, with the mathematician Jaroslav Blažek, began placing the elements of his paintings by computer in 1964, and from 1973 painted lines whose every number came from chance. Structure is the rule the two of them published in Leonardo in 1970: twenty elements of half-discs in four groups by how much of them is black, a chart with a few elements placed and plus and minus signs, and a walk left to right and back that takes each cell's group from the cells around it and keeps the elements whose color or shape continues, as one of four rules asks. The machine types its codes on the left sheet, and the canvas is painted one template at a time. Replayed against the chart and table printed with the article, this reading gives back the printed group for 182 of the 196 computed cells, and all but 8 of the printed elements pass the rule. Lines writes a score: a start in whole centimeters, a width, one to five of thirty paints mixed one to one the way pigments mix, and three to eight tangents, each joined to the next by an arc whose radius follows from their lengths and the angle between them, along the grid's eight directions, the clock's twelve, or every even degree. Every crossing goes over or under as a series of ones and twos decides. Both export SVG that gives the rule back: every element of the canvas and every sign of the chart, and every line's start, tangents, and width, with each settled crossing drawn again inside a clip of the place where the two lines overlap.

  • Measuring color. distance(to:) says how different two colors look, as a straight line in OKLab (the scale the palette check's tolerance already uses), or by the CIE's 2000 color-difference formula with measure: .ciede2000, which matches all 34 of the test pairs Sharma, Wu and Dalal published for checking implementations. CIELab brings in the Lab numbers swatch books and color pickers give, against the D50 white they are measured under. Palette.nearest(to:) returns the swatch that looks most like a color and nearestIndex(to:) its position. Image.histogram(of:bins:in:) counts a picture's pixels by lightness or by one channel, and percentile(_:) reads its working black and white. The Color/Shades example uses all three. Measuring color

  • Shades whose hue drifts. Palette.shades(of:count:) and Ramp.shades(of:) build a ramp of one color the way pixel artists and painters shade: the lights turn toward yellow and the shadows away from it, toward blue or purple, with the chroma eased at both ends. The base color is always one of the swatches, and hueDrift sets how far the hue turns, a negative drift cooling the lights instead. Shades

  • The slope of a fitted field. RadialBasis reports how its field changes: gradient(at:) for a field of numbers, the way uphill with a length that says how steep, and derivative(at:along:) for any field, a warp's stretch included. Both are exact and cost about one read of the value. For a function you wrote yourself, derivative(at:function:) and gradient(at:field:) measure the slope by central differences. The Motion/ContourWalkers example walks the contours of both kinds. Slopes

Changed

  • A caption draws over the finished frame. drawCaption now lands in the overlay, so it stays sharp and white over a depth of field, a motion blur, a bloom, or a lens flare, and never joins an accumulating canvas; inside a layer or a batch recording, where there is no overlay, it draws in place as before. drawStatus & drawCaption

  • A parameter read costs what a stored property costs. A @Param holding a number, an integer, or a switch now reads in under a nanosecond, down from about 40, so a loop may read one per vertex and the advice to copy parameters into locals first applies only to a color, a vector, a palette, or a text value, which still read through a lock. Parameters

  • 2D torque takes points, like force. applyTorque now takes a force in points times an arm in points, so the numbers that push a body also turn it; it took the engine's newton-meters before, so a sketch calling it spins about ten thousand times faster at the default scale and wants its torques divided by unitsPerMeter squared to keep its motion. Rigid bodies

  • The band-pass voice filter is unity at its cutoff. Resonance now only narrows the band; the output used to grow with it (five times louder at resonance 0.9), so a band-pass voice at high resonance plays quieter than before. Voice.Filter

  • An export's soft shadows sample 64 points on the light. Under temporal anti-aliasing an export traces one set of points on the light across its sixteen passes, and the .detail tier every export resolves to now takes 64 of them, four a pass, where the live tier's 16 showed a wide penumbra as sixteen steps; shadowSamples(_:) still passes an exact count through, and a window traces what it did. Soft shadows

  • A camera rig's framing follows its arguments. cameraShowcase, cameraControl and cameraMove used to read target, radius, elevation, azimuth and fieldOfView on their first call only, so a @Param passed in tuned nothing in the window while an export framed with it. The arguments are now the shot's home. A changed one glides the shot there over about half a second, under a move or the showcase's auto-orbit, and under cameraControl until the viewer takes the camera. After that it moves only where resetCamera() and the idle return go. The same values every frame change nothing, so a sketch passing constants draws exactly what it did. The from: forms take the authored camera's pose the same way. Showcase

  • A traced shadow under an export's temporal anti-aliasing costs a sixteenth of what it did. The export draws the scene sixteen times at sub-pixel offsets, and a traced point or area shadow traced its whole ray budget in each of them, the same points on the light every time. Each pass now traces its own share of one set, so the frame traces the budget once: 1,000 spheres under a point light at 1080 by 1920 went from 25 seconds a frame to 2.3, with the same penumbra. A shadowSamples count is now how many points on the light the whole frame traces, so under the sixteen passes any count up to 16 costs one ray a pass. Exports of traced shadows under temporal AA change by a fraction of a level along their penumbrae. Soft shadows

  • Even spacing on a surface is about three times faster. surfacePoints(on:count:) with its default .blueNoise thinning now searches only the cells its candidates occupy, where it walked a grid of the whole bounding box, and keeps one queue entry per candidate. 400,000 points on a terrain took 13.4 s in a release build and take 4.7 s, a second of which is drawing the candidates. The debug build that swift run makes gains more: there the thinning of those points went from three minutes to 20 seconds. The points are the same for every mesh, count, and seed, so a scatter laid down before looks exactly as it did. Points on a surface

  • MarkovChain is in the core. The chain the music helpers use to pick notes now lives in Ollin itself, since it picks colors and line lengths as readily as notes, and a drawing sketch no longer needs the audio library for it. Every seed walks the same way it did. A file that imports only OllinAudio and uses the chain now needs import Ollin beside it. It also takes a table of counts directly: learn(_:after:times:) records that a context was followed by an element a number of times, so a table counted off a picture or written by hand becomes a chain with no sequence behind it. MarkovChain, a choice that remembers

  • A color-vision simulation shows a dichromat colors they would see as themselves. At full severity Color.simulated(_:) and Filter.colorVision(_:) now project each color onto the surface of colors a dichromat can tell apart, the two half-planes of Brettel, Viénot and Mollon (1997), so simulating a simulated color leaves it where it is. The table they used before was not a projection: a blue moved 0.064 on a second pass, a purple 0.072 under tritanopia. A severity below 1 now moves each color that share of the way in a straight line in linear light, so turning the severity up only ever moves a color toward where the dichromat sees it; the old tritan dial passed its own end on the way. Grays and white come back exactly. A result past what the display can show is still clamped channel by channel, which changes what that viewer sees least. Pictures move, the tritan column most, and the safe set's closest pair reads 0.078 where it read 0.076. Breaking: ColorVision.matrix is removed, since every kind now has two transforms rather than one; applied(toLinear:_:_:) still gives the model's result for a linear color. Seeing your colors as somebody else does

Fixed

  • An overlay no longer loses the scene under a sky backdrop. The overlay pass drew the environment's backdrop a second time, full-frame and with no depth, so a withOverlay block (or a caption) over a scene lit by environment(.sky(...)) or an HDRI with its backdrop shown could cover every surface and keep only the sky and the block, where the camera's far plane let the backdrop through; the pass now starts from the finished frame and draws only the block. withOverlay

  • The probe grid keeps its planes off a scene's faces. The global-illumination grid is now laid so a scene's bounding faces sit half a cell inside the outer probe planes, where a face on a plane put a ragged dark seam on a room's open end (a row of probes at the walls' front edges, flipping in and out of the visibility test); the seam is gone at the framework, and a room no longer has to run past the camera to avoid it. Global illumination

  • A traced shadow's softness no longer follows the camera. A traced point light's area is now sized by its distance to the scene's center, where it followed the distance to the camera's target, so the penumbra stayed put only while the camera aimed at one spot (192 levels between two aims along one line) and breathed through a camera move. Soft shadows

  • A light's reach holds in the bounce and in the trace. The probe field and the path tracer now read a lamp's reach: as the raster does, so a lamp lights and bounces nothing past it anywhere, where the field used to fill a room the lamp could not reach and the tracer lit the floor past it. Reach

  • A sun brighter than a half float arrives whole. An HDRI is now decoded at single precision and scaled into half range by one factor the bake divides back out, so a sun of 200,000 lights what its pixels say, where the decode used to clamp it at 65,504 and deliver a third of its light. Environment lighting

  • The blobby cross ends at its tips. Its distance formula reported a fraction of the true distance past each tip, so the render drew a faint spike from every tip to the quad's edge; the diamond through the four tips now floors it, and a blobbiness below 0.35 draws to its radius where the tip used to fall short. drawBlobbyCross

  • A traced SVG outline keeps its sharp corners. The curved distance-field shapes trace their outline on a grid, which cut a sharp corner across by up to a cell's diagonal (2.4 px on the cut disk's corners, 1.7 on the horseshoe's arm ends); a cell whose two crossings disagree in normal now bends through the corner where the two sides meet. Vector: SVG

  • Text along a path fills the glyph the plain run fills. The per-glyph route (drawText(_:along:), perGlyph:) flattened each glyph afresh at render size, so the same letter carried 4.5% more ink along a path than in a plain run; it now takes the cached outline the plain run uses, cleaned of self-overlaps, so the two agree to a level. Per-glyph drawText

  • A sound export finds every instrument, on time, at the right level. Four leaks in the soundtrack an export writes, each measured in the survey below, are closed. An instrument held inside a struct, an array, an optional, a dictionary, or an object of the sketch's own is found and advanced like one held as a plain property, so the Spatial example's three chimes are back in its own file (the sketch's values are read through everything it holds, direct properties first, each object once, a list through its first element). A note asked for on a frame starts on that frame's first sample, where every note used to land one frame late (42 ms at 24 fps). A change of hearingDistance while the export runs reaches the file at the frame that made it, where the file used to hold the range the export began with. And unplace() plays the sound at the level it had before it was placed, in both ears, where it used to sit 6 dB down for the rest of the piece. The finder's rules are on the Synthesis page under Sound in an export. Sound in an export

  • A struck body no longer clips, and hardness is the color of the strike rather than its loudness. The strike's push is divided by its own area, so a softer strike reaches fewer tones instead of pushing the same tones harder, and a strike is as loud at 48 kHz as at 44.1. Before, every struck preset but .wood sat on the renderer's ceiling for most of its attack (the bell for 56% of its first second) and the clip put tones in the bell that it has no partial at. The presets had been tuned under that clip, so they strike harder now (.bell and .bar 0.9, .drum and .plate 0.85, .glass 0.8) and each rings the partials it declares; the default hardness of a ModalBody, ModalBody(shape:), and StruckShape.body moves from 0.7 to 0.85 so a drawn shape's upper modes are heard an octave up; and the struck Voice presets (.drum, .bar, .chime, .glass) carry gain 1, which puts them within 3 dB of .pluck at the same velocity. A sketch that balanced a struck instrument against the old clipped level will hear it a little quieter and a good deal cleaner; the SoundInAnExport and StruckShapes examples raised their struck instruments' gain to keep their balance. A struck body

  • Promises the reference no longer makes, after a survey of the sound, the color science, and the solvers. A plucked string's decay is exact at low notes and shorter at high ones; a bowed string's decay is a loss in its loop, not a ring-out, since a bow at pressure 0 rests on the string; a blown tube needs a minimum breath before it speaks (0.6 for the clarinet, 0.4 for the reedy one, 0.7 for the hollow one), and the pressure sweep the page shows now starts there; Delay.time runs up to 2 s; the limiter flattens a wave that crosses its ceiling, since it has no lookahead; the mixed soundtrack bends anything above 0.7, a single loud instrument included. A .wide or .extended frame skips the dither on its way to an eight-bit file, the wide PNG is written at eight bits with a linear profile, and a gain map's one percent holds across a highlight and its field, not at its edge. A Lab color outside sRGB keeps its OKLab lightness and hue, and the system's generic CMYK profile proofs alike under perceptual, saturation, and relative. In the physics, a particle's drag is taken once a step (45% of its speed a second at 60 Hz at the default) where a rigid body's is a loss per second and both reach a body or the walls when they are made; a body bounces off the walls or the ground with the larger of its own restitution and the world's; a spin is capped at a quarter turn a step; every 3D body loses 5% of its speed and spin each second; and a 3D frame a hair over 1/60 s takes two collision passes. Each gap is pinned by an invariant test that fails the day it closes, and filed in the design notes with the shape of its fix.

  • A background on the canvas leaves the layers alone. background(_:) outside a layer block used to drop everything the frame had recorded, the layers drawn before it included, so a withTarget layer, a withField seed, or a feedback loop drawn before the canvas background came out empty with no error, and the reference told you to call background first. It now clears the canvas and keeps every layer's marks, so the order no longer matters: a frame that draws a layer, then the canvas, then the background is the same picture, pixel for pixel, as one that sets the background first. A frame that sets its background first is unchanged. withTarget

  • Measured and written down where the vector outline and the render part. A test now checks, for all 34 distance-field shapes and point markers, that the outline the SVG export writes traces the shape the window draws. Three gaps stand as known issues with their numbers: the blobby cross draws a faint spike past each tip that its outline does not have, and the traced outlines of the cut disk and the horseshoe round their sharp corners off by about 2 px. A plain text run and the same text along a path or drawn glyph by glyph differ slightly at the curves (a 20 px O carries 4.5% more ink along a path); that is pinned the same way. Vector: SVG

  • Known gaps in the photoreal tier, measured and written down. Nineteen more physical invariants read the materials, the environment and the sky, the air, global illumination, the shadows, the path-traced export, the live temporal resolve, LineSpray, and the lens flare's coatings against analytic values and a deep path trace. Five hold outright, and fourteen gaps stand as known issues with their numbers (in CAPABILITIES.md and under Photorealistic 3D in DESIGN-NOTES.md). Among them: a rough white plastic loses about a tenth of its light at grazing under an environment, the procedural sky's sun casts almost no light of its own, and global illumination gathers no emission and ignores a lamp's reach:, as does the path tracer. The 3D reference no longer promises that emitters light their neighbors under global illumination (they do so in a path-traced export) or that 2D in a 3D canvas is untouched by the live temporal resolve while the camera is still.

  • A camera move starts at its start. A cameraMove or cameraShowcase move advanced its clock before posing the camera, so its first frame showed it one step in and exported frame N was posed at (N+1)/fps while time read N/fps. Two exports of one frame at different frame rates disagreed. The move now poses at its clock and then advances it, so frame N shows the move at N/fps. Every exported frame of a sketch with a camera move moves by one step. Camera moves

  • A move resumed after the hands starts from the hands. Switching from cameraControl() back to the same cameraMove it left resumed the move from where it first started, so the camera jumped away from the shot just framed by hand. It now departs from the current pose, as switching to a different move always did. Camera control

  • Exact black and exact white stay exact. The last pass adds up to a level of noise before the 8-bit encode so a smooth ramp does not band, and at the ends of the range that noise had nothing to do but harm: about one pixel in six of an exact black came out as 1, and one in eight of an exact white as 254. So a picture made only of black and white did not export as only black and white, and every export on a black ground fed a faint noise floor to the video encoder. The noise now reaches less far within a level of either end, never past it: exact black is 0 and exact white is 255 in every export and on the web page, gradients away from the ends dither as before, and values near an end average back to themselves more closely than they did. Color output

  • A motion streak keeps the light of what made it. motionBlur() had kept a mover's inside at its full value while adding the smear around it, so a moving thing grew brighter: a 24-pixel box crossing at 20 pixels a frame gained 5% of its light at the default shutter and 25% at a full one, and a bloom read the excess. The gather now weighs each tap by whether a streak reaches the pixel and leaves the weights as they are, so their mean is the share of the shutter the streak spent there and the pixel's own color fills the rest. The box keeps its light within 0.2%, and region by region the streak stays within 2% of the box's value of the subframe export. On the MotionBlur example the error around the spheres fell by two to three times, and the sharp ghost of a sphere inside its own streak is gone. A caption drawn over the 3D scene takes the motion of the surface under it, so draw it in withOverlay { }. Motion blur

  • A caustic gives back the light its shadow took, and no more. A clear glass ball's caustic had been adding half again the light its shadow removed, where real glass gives back less than it blocks. Two things made it bright: on a physically based floor the splat shaded its light at pi times what a direct light of the same strength gets, and it counted the angle the light arrives at twice. A half-float layer hid part of that, capping a focused spot at a power of two and shaving a few percent off everything else; the layer is single precision now. A caustic is now shaded exactly as the floor shades a direct light, on every shading model, and a clear pane gives back 92% of its shadow's light, a clear ball about 84% (the rest is what their surfaces reflect). Caustics are dimmer than before on a physically based floor; caustics(intensity:) above 1 turns them up past physical. Light leaving glass now reflects at the angle it exits at, a live caustic no longer dims for its first frames while its photon plan settles, and a point or spot light's caustic carries the light the lamp lays at the end of each photon's path, so a lamp's ball gives back what its shadow took rather than a third of it, a mirror throws exactly the light of the lamp's mirror image, and a lamp's reach thins its caustic as it thins its light. Caustics