Skip to content
Open
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
45 changes: 45 additions & 0 deletions dash-apps/contest/prag-designs-felina/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Felina

**Category:** Rive
**File:** [`felina.riv`](felina.riv) (about 260 KB), built from [`src/`](src/)

![Felina](preview.jpg)

Felina is a night-drive instrument cluster under glass. The name is Fe, Li and Na: iron for the car, lithium for the battery it reads, and sodium for the amber streetlight glow it is lit with. One big dial holds the speed: large digits in the hub, a needle with a fading light trail, the speed limit as a red mark on the scale, and the cruise set speed as a blue one. Two smaller dials sit beside it. On the left, battery charge and a power gauge that swings toward POWER or REGEN. On the right, your car seen from above, with cars in your blind spots, lane marks that pass at your speed, and the assist state. The light comes from the car's data. A traffic light floods the glass red, amber or green. The blinker throws amber light in from that side of the screen, timed to the real lamp. Over the limit, the scale between the limit mark and the needle glows hot. Regen sends energy streaming along the battery arc. When the dash starts, the dials push in, the readouts count up and the needle sweeps the whole scale before it settles.

## What it shows

| Data | How |
|---|---|
| `egoSpeed` | the big digits in the hub, and the needle on a 0 to 160 scale. It also sets how fast the lane marks pass |
| `isImperial` | KM/H or MPH under the digits |
| `fusedSpeedLimit` | the limit in a red ring above the digits and a red mark on the scale; hidden while unknown. Over it, the gap glows and a red +N appears |
| `accSetSpeed`, `adasOn` | a blue mark on the scale and SET under the digits; ASSIST ON or MANUAL in the right dial, and a cool light on the hub rim |
| `gear` | P R N D under the digits, the current one lit. In P the hub shows a large P and PARKED; in R a white reverse light rises along the bottom of the dial with REVERSE |
| `powerLevel`, `packVoltage`, `packCurrent` | the power gauge in the left dial with kW, and pack voltage in the bottom bar |
| `nominalEnergyRemaining`, `fullPackEnergy`, `energyBuffer` | battery % and the charge arc in the left dial |
| `leftBlinker`, `rightBlinker` | a green arrow beside the dial lit in step with the lamp, and amber light sweeping in from that edge, with a bulb-like afterglow |
| `leftBlindSpot`, `rightBlindSpot` | a car in the next lane in the right dial and a glow on that screen edge: amber for a vehicle, red when urgent. Shown whenever the car reports it |
| `egoSteeringAngle` | your car in the right dial turns with the wheel, and the reflection on the glass slides |
| `changingLane` | CHANGING LANE under the assist state |
| `trafficLightColor`, `stopLineDist` | three lamps in the right dial, the glass tinted the same colour, and the distance to the stop line |
| `anyDoorOpen`, `buckleStatus` | DOOR OPEN and SEATBELT in the top bar |
| `currentTime`, `phoneBattery`, `dataSourceType` | clock, phone battery (`showPhoneBattery`) and the data source in the top bar |
| `odometer` | odometer in the bottom bar (`showOdometer`) |
| `renderQuality` | the glass pass skips its bloom at the lowest setting |

The main artboard is built from layouts and tested from 1440 (4:3) to 2340 (19.5:9) units wide. The three dials live in their own `Gauges` artboard, placed with fit `scaleDown`: full size on a phone, shrunk as a whole on a tablet.

## How it is built

Everything you see is native Rive: shapes, text and data binds on `DashViewModel`. A scripted layout (`src/felina_fx.luau`) then draws a private copy of that instrument into a texture each frame and finishes it in a WGSL pass (`src/nightglass.wgsl`): bloom, glass, the needle trail and the lights listed above. While the script runs it sets `fxActive`, which hides the native copy underneath. If a runtime cannot run scripts, nothing in that layer draws and the native dash shows on its own, with every reading still live.

- `src/gen.py` writes `src/scene.rml` from `viewmodel.part`, `converters.part` and `mine.part`. One `.rml` file on purpose: a project split across several publishes script signatures the web runtime rejects.
- `src/viewmodel.part` is the `DashViewModel` from `rive/dash-demo/`, plus a few properties of my own that only the script writes (`fxActive`, `fxWidth`, `laneScroll`, `intro*`).
- Rebuild: `cd src && python3 gen.py && rive . --publish`. This writes `../felina.riv` with the two scripts signed. `rive . --once` builds without signing.

## Credits

- **Inter** font (`src/fonts/inter-400.ttf`, `inter-600.ttf`) by The Inter Project Authors, [SIL Open Font License 1.1](https://github.com/rsms/inter/blob/master/LICENSE.txt). These are the same subset files `rive/dash-demo/` already ships.
- **Car** (`src/car.png`, the top-down car in the right dial, also tinted for the blind-spot cars): made for this entry with OpenAI's image model, no reference image or third-party artwork. Released under this repository's MIT license with the rest of the entry.
- Everything else (dials, scales, needle, icons, lane marks, the shader and the start-up) is my own work in this folder. No other images, icons or sounds.
Binary file added dash-apps/contest/prag-designs-felina/felina.riv
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions dash-apps/contest/prag-designs-felina/src/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build/
__pycache__/
Binary file added dash-apps/contest/prag-designs-felina/src/car.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
270 changes: 270 additions & 0 deletions dash-apps/contest/prag-designs-felina/src/converters.part
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
<Rive version="1" kind="fragment">
<FontAsset file="fonts/inter-400.ttf" name="Inter Regular" id="2:900"/>
<FontAsset file="fonts/inter-600.ttf" name="Inter SemiBold" id="2:901"/>

<!-- Formula tokens: operationType 0 + 1 - 2 * 3 / 4 %; functionType 0 min 1 max 4 floor 5 sqrt 6 pow -->

<DataConverterToString decimals="0" round="true" name="Integer" id="2:1"/>
<!-- Booleans: ToString gives "0"/"1", ToNumber turns that into a number that can drive opacity. -->
<DataConverterToString name="Bool To Text" id="2:40"/>
<DataConverterToNumber name="Text To Number" id="2:42"/>
<DataConverterGroup name="Bool As Number" id="2:41">
<DataConverterGroupItem converterId="2:40" id="0:903"/>
<DataConverterGroupItem converterId="2:42" id="0:904"/>
</DataConverterGroup>
<DataConverterGroup name="Not Bool As Number" id="2:43">
<DataConverterGroupItem converterId="2:40" id="0:905"/>
<DataConverterGroupItem converterId="2:42" id="0:906"/>
<DataConverterGroupItem converterId="2:7" id="0:907"/>
</DataConverterGroup>
<DataConverterGroup name="Cruise Marker Opacity" id="2:44">
<DataConverterGroupItem converterId="2:40" id="0:908"/>
<DataConverterGroupItem converterId="2:42" id="0:909"/>
<DataConverterGroupItem converterId="2:45" id="0:910"/>
</DataConverterGroup>
<DataConverterRangeMapper minInput="0" maxInput="1" minOutput="0.35" maxOutput="1"
clampLower="true" clampUpper="true" name="Dim Unless On" id="2:45"/>
<DataConverterStringPad length="2" text="0" padType="start" name="Pad 2" id="2:27"/>

<DataConverterRangeMapper minInput="0" maxInput="1" minOutput="0" maxOutput="1"
clampLower="true" clampUpper="true" name="Clamp 0..1" id="2:5"/>
<DataConverterRangeMapper minInput="0" maxInput="1" minOutput="1" maxOutput="0"
clampLower="true" clampUpper="true" name="1 - Clamp 0..1" id="2:7"/>
<DataConverterRangeMapper minInput="1" maxInput="2" minOutput="0" maxOutput="1"
clampLower="true" clampUpper="true" name="Is 2" id="2:6"/>
<DataConverterRangeMapper minInput="0" maxInput="100" minOutput="100" maxOutput="0"
clampLower="true" clampUpper="true" name="100 - Percent" id="2:18"/>
<DataConverterRangeMapper minInput="0" maxInput="100" minOutput="0" maxOutput="100"
clampLower="true" clampUpper="true" name="Percent" id="2:22"/>

<DataConverterRangeMapper minInput="0" maxInput="160" minOutput="0" maxOutput="0.75"
clampLower="true" clampUpper="true" name="Speed To Arc" id="2:2"/>
<DataConverterRangeMapper minInput="0" maxInput="160" minOutput="3.9269908" maxOutput="8.6393798"
clampLower="true" clampUpper="true" name="Speed To Angle" id="2:3"/>
<DataConverterSystemDegsToRads operationType="2" operationValue="0.017453292"
name="Degrees to Radians" id="2:15"/>

<DataConverterFormula name="Over Limit" id="2:4">
<FormulaTokenFunction functionType="0" id="0:911"/>
<FormulaTokenValue operationValue="1" id="0:912"/>
<FormulaTokenArgumentSeparator id="0:913"/>
<FormulaTokenFunction functionType="1" id="0:914"/>
<FormulaTokenValue operationValue="0" id="0:915"/>
<FormulaTokenArgumentSeparator id="0:916"/>
<FormulaTokenParenthesisOpen id="0:917"/>
<FormulaTokenInput id="0:918"/>
<FormulaTokenOperation operationType="1" id="0:919"/>
<FormulaTokenValue id="0:920">
<DataBindContext sourcePathIds="1:1-1:19" propertyKey="777" id="0:921"/>
</FormulaTokenValue>
<FormulaTokenOperation operationType="1" id="0:922"/>
<FormulaTokenValue operationValue="2" id="0:923"/>
<FormulaTokenParenthesisClose id="0:924"/>
<FormulaTokenOperation operationType="3" id="0:925"/>
<FormulaTokenValue operationValue="3" id="0:926"/>
<FormulaTokenParenthesisClose id="0:927"/>
<FormulaTokenParenthesisClose id="0:928"/>
<FormulaTokenOperation operationType="2" id="0:929"/>
<FormulaTokenFunction functionType="0" id="0:930"/>
<FormulaTokenValue operationValue="1" id="0:931"/>
<FormulaTokenArgumentSeparator id="0:932"/>
<FormulaTokenValue id="0:933">
<DataBindContext sourcePathIds="1:1-1:19" propertyKey="777" id="0:934"/>
</FormulaTokenValue>
<FormulaTokenParenthesisClose id="0:935"/>
</DataConverterFormula>



<!-- 1 - min(1, |input - k|): 1 when input == k -->
<DataConverterFormula name="Equals 1" id="2:11">
<FormulaTokenValue operationValue="1" id="0:936"/>
<FormulaTokenOperation operationType="1" id="0:937"/>
<FormulaTokenFunction functionType="0" id="0:938"/>
<FormulaTokenValue operationValue="1" id="0:939"/>
<FormulaTokenArgumentSeparator id="0:940"/>
<FormulaTokenFunction functionType="5" id="0:941"/>
<FormulaTokenFunction functionType="6" id="0:942"/>
<FormulaTokenInput id="0:943"/>
<FormulaTokenOperation operationType="1" id="0:944"/>
<FormulaTokenValue operationValue="1" id="0:945"/>
<FormulaTokenArgumentSeparator id="0:946"/>
<FormulaTokenValue operationValue="2" id="0:947"/>
<FormulaTokenParenthesisClose id="0:948"/>
<FormulaTokenParenthesisClose id="0:949"/>
<FormulaTokenParenthesisClose id="0:950"/>
</DataConverterFormula>
<DataConverterFormula name="Equals 2" id="2:12">
<FormulaTokenValue operationValue="1" id="0:951"/>
<FormulaTokenOperation operationType="1" id="0:952"/>
<FormulaTokenFunction functionType="0" id="0:953"/>
<FormulaTokenValue operationValue="1" id="0:954"/>
<FormulaTokenArgumentSeparator id="0:955"/>
<FormulaTokenFunction functionType="5" id="0:956"/>
<FormulaTokenFunction functionType="6" id="0:957"/>
<FormulaTokenInput id="0:958"/>
<FormulaTokenOperation operationType="1" id="0:959"/>
<FormulaTokenValue operationValue="2" id="0:960"/>
<FormulaTokenArgumentSeparator id="0:961"/>
<FormulaTokenValue operationValue="2" id="0:962"/>
<FormulaTokenParenthesisClose id="0:963"/>
<FormulaTokenParenthesisClose id="0:964"/>
<FormulaTokenParenthesisClose id="0:965"/>
</DataConverterFormula>
<DataConverterFormula name="Equals 3" id="2:13">
<FormulaTokenValue operationValue="1" id="0:966"/>
<FormulaTokenOperation operationType="1" id="0:967"/>
<FormulaTokenFunction functionType="0" id="0:968"/>
<FormulaTokenValue operationValue="1" id="0:969"/>
<FormulaTokenArgumentSeparator id="0:970"/>
<FormulaTokenFunction functionType="5" id="0:971"/>
<FormulaTokenFunction functionType="6" id="0:972"/>
<FormulaTokenInput id="0:973"/>
<FormulaTokenOperation operationType="1" id="0:974"/>
<FormulaTokenValue operationValue="3" id="0:975"/>
<FormulaTokenArgumentSeparator id="0:976"/>
<FormulaTokenValue operationValue="2" id="0:977"/>
<FormulaTokenParenthesisClose id="0:978"/>
<FormulaTokenParenthesisClose id="0:979"/>
<FormulaTokenParenthesisClose id="0:980"/>
</DataConverterFormula>
<DataConverterFormula name="Equals 4" id="2:14">
<FormulaTokenValue operationValue="1" id="0:981"/>
<FormulaTokenOperation operationType="1" id="0:982"/>
<FormulaTokenFunction functionType="0" id="0:983"/>
<FormulaTokenValue operationValue="1" id="0:984"/>
<FormulaTokenArgumentSeparator id="0:985"/>
<FormulaTokenFunction functionType="5" id="0:986"/>
<FormulaTokenFunction functionType="6" id="0:987"/>
<FormulaTokenInput id="0:988"/>
<FormulaTokenOperation operationType="1" id="0:989"/>
<FormulaTokenValue operationValue="4" id="0:990"/>
<FormulaTokenArgumentSeparator id="0:991"/>
<FormulaTokenValue operationValue="2" id="0:992"/>
<FormulaTokenParenthesisClose id="0:993"/>
<FormulaTokenParenthesisClose id="0:994"/>
<FormulaTokenParenthesisClose id="0:995"/>
</DataConverterFormula>

<DataConverterFormula name="Urgent Left" id="2:33">
<FormulaTokenFunction functionType="0" id="0:996"/>
<FormulaTokenValue operationValue="1" id="0:997"/>
<FormulaTokenArgumentSeparator id="0:998"/>
<FormulaTokenFunction functionType="1" id="0:999"/>
<FormulaTokenValue operationValue="0" id="0:1000"/>
<FormulaTokenArgumentSeparator id="0:1001"/>
<FormulaTokenInput id="0:1002"/>
<FormulaTokenOperation operationType="1" id="0:1003"/>
<FormulaTokenValue operationValue="1" id="0:1004"/>
<FormulaTokenParenthesisClose id="0:1005"/>
<FormulaTokenParenthesisClose id="0:1006"/>
</DataConverterFormula>
<DataConverterFormula name="Urgent Right" id="2:34">
<FormulaTokenFunction functionType="0" id="0:1007"/>
<FormulaTokenValue operationValue="1" id="0:1008"/>
<FormulaTokenArgumentSeparator id="0:1009"/>
<FormulaTokenFunction functionType="1" id="0:1010"/>
<FormulaTokenValue operationValue="0" id="0:1011"/>
<FormulaTokenArgumentSeparator id="0:1012"/>
<FormulaTokenInput id="0:1013"/>
<FormulaTokenOperation operationType="1" id="0:1014"/>
<FormulaTokenValue operationValue="1" id="0:1015"/>
<FormulaTokenParenthesisClose id="0:1016"/>
<FormulaTokenParenthesisClose id="0:1017"/>
</DataConverterFormula>

<!-- state of charge: (remaining - buffer) / (full - buffer) * 100 -->
<DataConverterFormula name="State Of Charge" id="2:16">
<FormulaTokenFunction functionType="1" id="0:1018"/>
<FormulaTokenValue operationValue="0" id="0:1019"/>
<FormulaTokenArgumentSeparator id="0:1020"/>
<FormulaTokenFunction functionType="0" id="0:1021"/>
<FormulaTokenValue operationValue="100" id="0:1022"/>
<FormulaTokenArgumentSeparator id="0:1023"/>
<FormulaTokenParenthesisOpen id="0:1024"/>
<FormulaTokenInput id="0:1025"/>
<FormulaTokenOperation operationType="1" id="0:1026"/>
<FormulaTokenValue id="0:1027">
<DataBindContext sourcePathIds="1:1-1:28" propertyKey="777" id="0:1028"/>
</FormulaTokenValue>
<FormulaTokenParenthesisClose id="0:1029"/>
<FormulaTokenOperation operationType="3" id="0:1030"/>
<FormulaTokenFunction functionType="1" id="0:1031"/>
<FormulaTokenValue operationValue="0.01" id="0:1032"/>
<FormulaTokenArgumentSeparator id="0:1033"/>
<FormulaTokenValue id="0:1034">
<DataBindContext sourcePathIds="1:1-1:27" propertyKey="777" id="0:1035"/>
</FormulaTokenValue>
<FormulaTokenOperation operationType="1" id="0:1036"/>
<FormulaTokenValue id="0:1037">
<DataBindContext sourcePathIds="1:1-1:28" propertyKey="777" id="0:1038"/>
</FormulaTokenValue>
<FormulaTokenParenthesisClose id="0:1039"/>
<FormulaTokenOperation operationType="2" id="0:1040"/>
<FormulaTokenValue operationValue="100" id="0:1041"/>
<FormulaTokenParenthesisClose id="0:1042"/>
<FormulaTokenParenthesisClose id="0:1043"/>
</DataConverterFormula>
<DataConverterGroup name="State Of Charge Text" id="2:17">
<DataConverterGroupItem converterId="2:16" id="0:1044"/>
<DataConverterGroupItem converterId="2:1" id="0:1045"/>
</DataConverterGroup>
<DataConverterGroup name="State Of Charge Rest" id="2:19">
<DataConverterGroupItem converterId="2:16" id="0:1046"/>
<DataConverterGroupItem converterId="2:18" id="0:1047"/>
</DataConverterGroup>

<DataConverterRangeMapper minInput="-100" maxInput="0" minOutput="100" maxOutput="0"
clampLower="true" clampUpper="true" name="Regen" id="2:20"/>
<DataConverterRangeMapper minInput="-100" maxInput="0" minOutput="0" maxOutput="100"
clampLower="true" clampUpper="true" name="Regen Rest" id="2:21"/>

<!-- packCurrent * packVoltage / 1000 -->
<DataConverterFormula name="Kilowatts" id="2:24">
<FormulaTokenInput id="0:1048"/>
<FormulaTokenOperation operationType="2" id="0:1049"/>
<FormulaTokenValue id="0:1050">
<DataBindContext sourcePathIds="1:1-1:30" propertyKey="777" id="0:1051"/>
</FormulaTokenValue>
<FormulaTokenOperation operationType="3" id="0:1052"/>
<FormulaTokenValue operationValue="1000" id="0:1053"/>
</DataConverterFormula>
<DataConverterGroup name="Kilowatts Text" id="2:25">
<DataConverterGroupItem converterId="2:24" id="0:1054"/>
<DataConverterGroupItem converterId="2:1" id="0:1055"/>
</DataConverterGroup>

<DataConverterFormula name="Hours" id="2:26">
<FormulaTokenFunction functionType="4" id="0:1056"/>
<FormulaTokenInput id="0:1057"/>
<FormulaTokenOperation operationType="3" id="0:1058"/>
<FormulaTokenValue operationValue="3600" id="0:1059"/>
<FormulaTokenParenthesisClose id="0:1060"/>
</DataConverterFormula>
<DataConverterGroup name="Hours Text" id="2:28">
<DataConverterGroupItem converterId="2:26" id="0:1061"/>
<DataConverterGroupItem converterId="2:1" id="0:1062"/>
<DataConverterGroupItem converterId="2:27" id="0:1063"/>
</DataConverterGroup>
<DataConverterFormula name="Minutes" id="2:29">
<FormulaTokenFunction functionType="4" id="0:1064"/>
<FormulaTokenParenthesisOpen id="0:1065"/>
<FormulaTokenInput id="0:1066"/>
<FormulaTokenOperation operationType="4" id="0:1067"/>
<FormulaTokenValue operationValue="3600" id="0:1068"/>
<FormulaTokenParenthesisClose id="0:1069"/>
<FormulaTokenOperation operationType="3" id="0:1070"/>
<FormulaTokenValue operationValue="60" id="0:1071"/>
<FormulaTokenParenthesisClose id="0:1072"/>
</DataConverterFormula>
<DataConverterGroup name="Minutes Text" id="2:30">
<DataConverterGroupItem converterId="2:29" id="0:1073"/>
<DataConverterGroupItem converterId="2:1" id="0:1074"/>
<DataConverterGroupItem converterId="2:27" id="0:1075"/>
</DataConverterGroup>

<DataConverterRangeMapper minInput="0" maxInput="100" minOutput="0" maxOutput="34"
clampLower="true" clampUpper="true" name="Phone Level Width" id="2:31"/>
<DataConverterRangeMapper minInput="-1" maxInput="0" minOutput="0" maxOutput="1"
clampLower="true" clampUpper="true" name="Phone Known" id="2:32"/>
</Rive>
Loading
Loading