Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .claude/skills/screenshots/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ only evidence that counts.
ruby Tools/ipad-shots.rb # iPad: 4 shots × 2 languages, ~8 min
ruby Tools/macos-shots.rb # Mac: 4 shots × 2 languages
ruby Tools/visionos-shots.rb # Vision Pro: 3 shots × 2 languages
ruby Tools/screenshots.rb # always: strip alpha, optimise, rebuild site/shots
ruby Tools/screenshots.rb # always: strip alpha, optimise, rebuild site/shots and docs/
```

All three take a name filter (`ruby Tools/ipad-shots.rb star`) and end by
Expand Down Expand Up @@ -53,7 +53,9 @@ composite. A capture with *real* transparency stops the run instead, because
choosing a background would change the picture and that is a person's decision.

It then runs `oxipng -o max --strip safe`, which is lossless and worth about a
fifth of the bytes, and regenerates `site/shots/`.
fifth of the bytes, and regenerates `site/shots/` — and the README's two
pictures, `docs/Screenshot.png` (the Mac tree capture) and `docs/Viewer.png`
(the Vision Pro one), which are derived on exactly the same terms.

**That last part is the reason it is a script and not a paragraph.** The site
images are downscaled copies of seven captures, so a reshoot that stops at
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ xcrun simctl io <device> screenshot shot.png # 3840x2160, with an alpha chan

# The App Store listing (appstore/). The check needs no key and no bundle;
# the other two need ASC_ISSUER_ID / ASC_KEY_ID / ASC_PRIVATE_KEY_PATH.
ruby Tools/screenshots.rb # after ANY reshoot: strip alpha, optimise, rebuild site/shots
ruby Tools/screenshots.rb # after ANY reshoot: strip alpha, optimise, rebuild site/shots and docs/
ruby fastlane/metadata_check.rb # what CI runs on every pull request
bundle exec fastlane ios metadata_diff # live listing vs what is written
bundle exec fastlane ios metadata_push # upload (mac for the other listing)
Expand Down
50 changes: 37 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ graphics engine written in Swift.
[**App Store**](https://apps.apple.com/app/id6798677334) ·
[Privacy](https://temoki.github.io/TortoiseBlocks/privacy.html)

<img src="docs/Screenshot.png" width="640" alt="TortoiseBlocks on macOS" />
<img src="docs/Screenshot.png" width="640" alt="Tortoise Blocks on macOS: a block named 🌳 that calls itself twice, and the fractal tree it draws." />

## Features

Expand All @@ -41,6 +41,11 @@ graphics engine written in Swift.
- **Blocks → Swift** — a syntax-colored code pane shows the equivalent
[Tortoise API](https://github.com/temoki/TortoiseGraphics2) program, as a
bridge from blocks to text programming
- **A viewer on Apple Vision Pro** — drawings are made on iPad and Mac; open
one on Vision Pro and it lies on a real table in front of you, at whatever
size you like, with a 3D tortoise standing on the paper walking the line as
it appears. The blocks and the generated Swift open in windows either side of
it, so a headset never has to choose between them the way one screen does
- **Documents** — a standard document app: `.tortoise` files (JSON),
iCloud Drive / Files integration, its own folder under On My iPad, autosave,
system undo; a new document can start from a sample program
Expand All @@ -53,11 +58,14 @@ graphics engine written in Swift.
- **English / Japanese** — Japanese uses kid-friendly hiragana; adding a
language is a single string-catalog edit

<img src="docs/Viewer.png" width="640" alt="A drawing open on Apple Vision Pro: the blocks in a window on the left, the transport in the middle, the generated Swift on the right, and the star itself on a sheet on the table with the tortoise standing on it." />

## Requirements

- **Xcode** 26+ (Swift 6.2)
- **Platforms** iPadOS 26+ · macOS 26+ · visionOS 26+ (the same three-pane app,
in a window on Vision Pro)
- **Platforms** iPadOS 26+ · macOS 26+ · visionOS 26+ — the three-pane editor
on iPad and Mac, and on Vision Pro a viewer for what they made, with no
editing in it at all

## Getting Started

Expand All @@ -77,15 +85,18 @@ swift test

```
TortoiseBlocks/
├── TortoiseBlocksKit/ # UI-independent SwiftPM package (depends on TortoiseCore only)
│ ├── Model/ # Block tree, frozen JSON format, pure editing functions
│ ├── Engine/ # BlockExpander: block tree → command stream (+ blockID tags)
│ └── CodeGen/ # SwiftCodeGenerator: block tree → Swift source (+ tokenizer)
├── App/ # SwiftUI document app (palette | workspace | canvas)
├── ThumbnailExtension/ # QuickLook thumbnails — reads one field, links nothing
├── appstore/ # The store listing: text per locale, screenshots per platform
├── fastlane/ # Pushes appstore/ to App Store Connect. The only Ruby here
└── site/ # The published website (GitHub Pages); docs/ is not published
├── TortoiseBlocksKit/ # UI-independent SwiftPM package (depends on TortoiseCore only)
│ ├── Model/ # Block tree, frozen JSON format, pure editing functions
│ ├── Engine/ # BlockExpander: block tree → command stream (+ blockID tags)
│ └── CodeGen/ # SwiftCodeGenerator: block tree → Swift source (+ tokenizer)
├── App/ # SwiftUI document app (palette | workspace | canvas)
│ └── Views/Viewer/ # visionOS only: the drawing on the table, and its three windows
├── ThumbnailExtension/ # QuickLook thumbnails — reads one field, links nothing
├── TortoiseBlocksUITests/ # Not a test suite so much as a camera: it shoots the captures
├── Tools/ # The capture rigs, and the Blender script the 3D tortoise comes from
├── appstore/ # The store listing: text per locale, screenshots per platform
├── fastlane/ # Pushes appstore/ to App Store Connect
└── site/ # The published website (GitHub Pages); docs/ is not published
```

The runtime pipeline is one straight line:
Expand Down Expand Up @@ -184,9 +195,22 @@ goes up on demand, by hand:
```bash
bundle exec fastlane metadata_check # the files alone, no network, no key
bundle exec fastlane ios metadata_diff # live listing against what is written
bundle exec fastlane ios metadata_push # upload (mac for the other listing)
bundle exec fastlane ios metadata_push # upload (mac, visionos for the others)
```

There are three listings and two sets of text: iOS and macOS share
`appstore/metadata`, while visionOS reads `appstore/metadata-visionos`, because
the app there is a viewer and the description that sells the editor would be
describing a product that does not exist. The app-level fields the App Store
keeps once per app rather than per platform — name, subtitle, privacy URL —
are checked byte-identical across both, since whichever lane runs last would
otherwise quietly overwrite the others.

The captures are made by the rigs in [Tools/](Tools/): one command each for
iPad, Mac and Vision Pro, all ending in `Tools/screenshots.rb`, which flattens
the alpha channel App Store Connect rejects, optimises every PNG, and
regenerates the website's downscaled copies from the same pictures.

## License

[MIT](LICENSE)
29 changes: 29 additions & 0 deletions Tools/screenshots.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
ROOT = Pathname.new(__dir__).parent
SCREENSHOTS = ROOT / "appstore" / "screenshots"
SITE_SHOTS = ROOT / "site" / "shots"
DOCS = ROOT / "docs"

# Which captures the website uses, and how big. A curated subset rather than a
# rule — the code pane is on the Mac half of the page and not the iPad half —
Expand Down Expand Up @@ -69,6 +70,23 @@
}
}.freeze

# The README's two pictures, on the same terms as the site copies and for the
# same reason: what a reshoot silently leaves behind is the picture nobody has
# open while shooting. Same recipe, too — quantised to 256 colours — which is
# what makes rerunning this reproduce the committed files rather than rewrite
# them.
#
# The Mac one is the *tree* on purpose. It is the only capture in the set that
# shows a block calling itself, which is what the README spends its longest
# bullet on, and the platform section is the Mac's other job on the page
# already. Both README images were composed by hand before the rigs existed —
# the same scenes, shot separately — so this refreshes them rather than
# replacing them, and the window sits a little differently for it.
DOCS_DERIVED = {
"macos/en-US/4_tree_canvas" => { target: "Screenshot.png", size: "2560x1600" },
"visionos/en-US/1_star_table" => { target: "Viewer.png", size: "1600x900" }
}.freeze

def run(*command)
return if system(*command)

Expand Down Expand Up @@ -134,4 +152,15 @@ def flatten(path)
run("oxipng", "-q", "-o", "max", "--strip", "safe", *Pathname.glob(SITE_SHOTS / "*.png").map(&:to_s))
puts "site/shots regenerated"

DOCS_DERIVED.each do |capture, spec|
source = SCREENSHOTS / "#{capture}.png"
next warn("missing #{relative(source)}, skipping docs/#{spec[:target]}") unless source.exist?

target = DOCS / spec[:target]
run("magick", source.to_s, "-resize", spec[:size], "-dither", "None",
"-colors", "256", "-strip", target.to_s)
run("oxipng", "-q", "-o", "max", "--strip", "safe", target.to_s)
end
puts "docs images regenerated"

exit(MetadataCheck.report ? 0 : 1)
Binary file modified docs/Screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/Viewer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.