Initial commit

This commit is contained in:
2026-04-15 00:51:17 +02:00
commit 0fec97163d
72 changed files with 3076 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
name: CI
on:
push:
pull_request:
jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
- name: Root pub get
run: flutter pub get
- name: Example pub get
working-directory: example
run: flutter pub get
- name: Format check
run: dart format --output=none --set-exit-if-changed .
- name: Analyze root
run: flutter analyze
- name: Tests
run: flutter test
- name: Outdated root
run: flutter pub outdated
- name: Outdated example
working-directory: example
run: flutter pub outdated
- name: Dartdoc dry-run
run: dart doc --dry-run
- name: Publish dry-run
run: dart pub publish --dry-run
- name: Pana
run: |
dart pub global activate pana
pana .
+45
View File
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
+30
View File
@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "db50e20168db8fee486b9abf32fc912de3bc5b6a"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
- platform: android
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+45
View File
@@ -0,0 +1,45 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "shiny",
"request": "launch",
"type": "dart"
},
{
"name": "shiny (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "shiny (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "example",
"cwd": "example",
"request": "launch",
"type": "dart"
},
{
"name": "example (profile mode)",
"cwd": "example",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "example (release mode)",
"cwd": "example",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}
+8
View File
@@ -0,0 +1,8 @@
# Changelog
## 0.1.0
- Initial release of `holo_shiny`.
- Added `Shiny` widget with shader-driven holographic effects.
- Added optional `ShinyController` and `SensorTiltController` motion APIs.
- Added package example app and baseline tests.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+142
View File
@@ -0,0 +1,142 @@
# holo_shiny
`holo_shiny` is a Flutter package that renders shader-driven holographic shine.
It exposes two widget layers:
- `Shiny`: apply shine to any widget (no clipping and no rotation)
- `ShinyCard`: card-oriented convenience widget with shape clipping and tilt rotation
## Features
- GLSL shader-driven holographic effects
- Optional motion input via sensors or custom tilt stream
- Generic wrapper API for any widget via `Shiny(child: ...)`
- Card API with `background`, `foreground`, `shape`, and drag tilt via `ShinyCard`
- Built-in sparkle presets including 8-point star, 5-point star, rectangle, diamond, hexagon, random polygon, and confetti
- Custom sparkle shapes via parameterized `SparkleShapeSpec` factories
- Cross-platform Flutter support (mobile, web, desktop)
## Installation
Add the package:
```yaml
dependencies:
holo_shiny: ^0.1.0
```
Then run:
```bash
flutter pub get
```
## Quick Start
```dart
import 'package:flutter/material.dart';
import 'package:holo_shiny/holo_shiny.dart';
class Demo extends StatelessWidget {
const Demo({super.key});
@override
Widget build(BuildContext context) {
return const Shiny(
child: ColoredBox(
color: Color(0xFF202A3A),
child: SizedBox(width: 280, height: 80),
),
);
}
}
```
## Card Usage
```dart
ShinyCard(
controller: controller,
background: Container(color: const Color(0xFF1B2D4B)),
foreground: const Center(child: Text('HOLO')),
sparkleShape: SparkleShapeSpec.eightPointStar,
)
```
## Motion with Controller
```dart
final ShinyController controller = ShinyController(useSensor: true);
Shiny(
controller: controller,
child: Container(color: const Color(0xFF1B2D4B)),
)
```
## Sparkle Shapes
Use built-in sparkle presets:
```dart
ShinyCard(
sparkleShape: SparkleShapeSpec.hexagon,
)
```
Or create your own parameterized shapes:
```dart
ShinyCard(
sparkleShape: SparkleShapeSpec.customStar(
points: 7,
innerRatio: 0.36,
),
)
Shiny(
sparkleShape: SparkleShapeSpec.customPolygon(
sides: 8,
aspectRatio: 1.2,
rotation: 0.2,
),
child: const SizedBox(width: 240, height: 120),
)
```
You can also provide a custom stream:
```dart
final StreamController<Offset> input = StreamController<Offset>.broadcast();
final ShinyController controller = ShinyController(tiltStream: input.stream);
```
## API Summary
- `Shiny`: generic effect wrapper
- `ShinyCard`: shape + rotation + composition convenience widget
- `SparkleShapeSpec`: built-in and custom sparkle silhouette configuration
- `ShinyController`: optional source selection for tilt
- `SensorTiltController`: low-level sensor fusion stream utility
## Platform Notes
- Shader uses Flutter runtime effects and avoids derivative functions (`dFdx`, `dFdy`, `fwidth`) for web compatibility.
- If a sensor is unavailable, motion input simply emits no events.
## Example
See the package example app in `example/` for:
- Basic usage
- Any-widget shiny wrapper usage
- Sensor-driven motion
- External stream override
- Custom card shape/background/foreground composition
- Built-in sparkle shape toggles in the demo UI
- User-defined sparkle shape presets using `SparkleShapeSpec`
## Publishing Notes
Replace placeholder package metadata URLs in `pubspec.yaml` before publishing.
+1
View File
@@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
+44
View File
@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.holo_shiny"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.holo_shiny"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+45
View File
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="holo_shiny"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.example.holo_shiny
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+2
View File
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
+5
View File
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
+45
View File
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
+30
View File
@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "db50e20168db8fee486b9abf32fc912de3bc5b6a"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
- platform: android
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+3
View File
@@ -0,0 +1,3 @@
# holo_shiny_example
A new Flutter project.
+1
View File
@@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
+44
View File
@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.holo_shiny_example"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.holo_shiny_example"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="holo_shiny_example"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.example.holo_shiny_example
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+2
View File
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

+418
View File
@@ -0,0 +1,418 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:holo_shiny/holo_shiny.dart';
void main() {
runApp(const ExampleApp());
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF24A6A8)),
),
home: const ExampleHome(),
);
}
}
class ExampleHome extends StatefulWidget {
const ExampleHome({super.key});
@override
State<ExampleHome> createState() => _ExampleHomeState();
}
class _ExampleHomeState extends State<ExampleHome> {
late final ShinyController _sensorController;
late final StreamController<Offset> _externalTiltController;
late final ShinyController _overrideController;
double _prismatic = 0.8;
double _sparkle = 0.8;
double _specular = 0.8;
double _diffraction = 0.8;
HolographStyle _style = HolographStyle.crackedIce;
SparkleShapeSpec _sparkleShape = SparkleShapeSpec.eightPointStar;
static const Map<String, SparkleShapeSpec> _sparkleChoices =
<String, SparkleShapeSpec>{
'8-Point Star': SparkleShapeSpec.eightPointStar,
'5-Point Star': SparkleShapeSpec.fivePointStar,
'Rectangle': SparkleShapeSpec.rectangle,
'Diamond': SparkleShapeSpec.diamond,
'Hexagon': SparkleShapeSpec.hexagon,
'Random Polygon': SparkleShapeSpec.randomPolygon,
'Confetti': SparkleShapeSpec.confetti,
};
@override
void initState() {
super.initState();
_sensorController = ShinyController(useSensor: true);
_externalTiltController = StreamController<Offset>.broadcast();
_overrideController =
ShinyController(tiltStream: _externalTiltController.stream);
}
@override
void dispose() {
_sensorController.dispose();
_overrideController.dispose();
_externalTiltController.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D121A),
appBar: AppBar(
title: const Text('holo_shiny example'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
const Text('Shiny on Any Widget'),
const SizedBox(height: 8),
Center(
child: SizedBox(
width: 320,
height: 120,
child: Shiny(
controller: _sensorController,
style: _style,
sparkleShape: _sparkleShape,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF18263E),
borderRadius: BorderRadius.circular(14),
),
child: const Row(
children: <Widget>[
Icon(Icons.auto_awesome, color: Colors.white),
SizedBox(width: 12),
Expanded(
child: Text(
'This can wrap any widget, including app chrome.',
style: TextStyle(color: Colors.white),
),
),
],
),
),
),
),
),
const SizedBox(height: 24),
const Text('ShinyCard (shape + rotation + shine)'),
const SizedBox(height: 8),
Center(
child: ShinyCard(
controller: _sensorController,
style: _style,
sparkleShape: _sparkleShape,
background: const _DemoCardBackground(),
foreground: const _RareBadge(),
prismatic: _prismatic,
sparkle: _sparkle,
specular: _specular,
diffraction: _diffraction,
),
),
const SizedBox(height: 16),
_StylePicker(
selectedStyle: _style,
onChanged: (HolographStyle style) {
setState(() {
_style = style;
});
},
),
const SizedBox(height: 8),
_SparkleShapePicker(
selectedShape: _sparkleShape,
choices: _sparkleChoices,
onChanged: (SparkleShapeSpec shape) {
setState(() {
_sparkleShape = shape;
});
},
),
const SizedBox(height: 8),
_LabeledSlider('Prismatic', _prismatic, (double value) {
setState(() {
_prismatic = value;
});
}),
_LabeledSlider('Sparkle', _sparkle, (double value) {
setState(() {
_sparkle = value;
});
}),
_LabeledSlider('Specular', _specular, (double value) {
setState(() {
_specular = value;
});
}),
_LabeledSlider('Diffraction', _diffraction, (double value) {
setState(() {
_diffraction = value;
});
}),
const SizedBox(height: 24),
const Text('External Tilt Stream + Custom Shape (card)'),
const SizedBox(height: 8),
Row(
children: <Widget>[
Expanded(
child: ElevatedButton(
onPressed: () =>
_externalTiltController.add(const Offset(0.7, 0.0)),
child: const Text('Tilt Right'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: () =>
_externalTiltController.add(const Offset(-0.7, 0.0)),
child: const Text('Tilt Left'),
),
),
],
),
const SizedBox(height: 8),
Center(
child: ShinyCard(
controller: _overrideController,
style: _style,
sparkleShape: _sparkleShape,
shape: const StadiumBorder(),
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: <Color>[Color(0xFF1A2A44), Color(0xFF15263D)],
),
),
),
foreground: const Center(
child: Text(
'EXTERNAL STREAM',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
letterSpacing: 1.2,
),
),
),
),
),
],
),
),
);
}
}
class _LabeledSlider extends StatelessWidget {
const _LabeledSlider(this.label, this.value, this.onChanged);
final String label;
final double value;
final ValueChanged<double> onChanged;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(label),
Slider(
value: value,
onChanged: onChanged,
),
],
);
}
}
class _StylePicker extends StatelessWidget {
const _StylePicker({
required this.selectedStyle,
required this.onChanged,
});
final HolographStyle selectedStyle;
final ValueChanged<HolographStyle> onChanged;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text('Style'),
const SizedBox(height: 6),
Wrap(
spacing: 8,
runSpacing: 8,
children: HolographStyle.values.map((HolographStyle style) {
return ChoiceChip(
label: Text(_styleLabel(style)),
selected: selectedStyle == style,
onSelected: (bool selected) {
if (!selected) {
return;
}
onChanged(style);
},
);
}).toList(),
),
],
);
}
String _styleLabel(HolographStyle style) {
switch (style) {
case HolographStyle.holographicSilver:
return 'Holographic Silver';
case HolographStyle.crackedIce:
return 'Cracked Ice';
case HolographStyle.silverMosaic:
return 'Silver Mosaic';
case HolographStyle.superGoldVinyl:
return 'Super Gold Vinyl';
}
}
}
class _SparkleShapePicker extends StatelessWidget {
const _SparkleShapePicker({
required this.selectedShape,
required this.choices,
required this.onChanged,
});
final SparkleShapeSpec selectedShape;
final Map<String, SparkleShapeSpec> choices;
final ValueChanged<SparkleShapeSpec> onChanged;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text('Sparkle Shape'),
const SizedBox(height: 6),
Wrap(
spacing: 8,
runSpacing: 8,
children:
choices.entries.map((MapEntry<String, SparkleShapeSpec> entry) {
return ChoiceChip(
label: Text(entry.key),
selected: selectedShape == entry.value,
onSelected: (bool selected) {
if (!selected) {
return;
}
onChanged(entry.value);
},
);
}).toList(),
),
],
);
}
}
class _DemoCardBackground extends StatelessWidget {
const _DemoCardBackground();
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: <Widget>[
Image.asset(
'assets/pokemon.png',
fit: BoxFit.cover,
alignment: Alignment.topCenter,
errorBuilder:
(BuildContext context, Object error, StackTrace? stackTrace) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: <Color>[Color(0xFF2D1B1B), Color(0xFF120D18)],
),
),
padding: const EdgeInsets.all(16),
child: const Align(
alignment: Alignment.bottomLeft,
child: Text(
'Demo Card',
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.w900,
),
),
),
);
},
),
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.black.withValues(alpha: 0.10),
Colors.black.withValues(alpha: 0.22),
],
),
),
),
],
);
}
}
class _RareBadge extends StatelessWidget {
const _RareBadge();
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topRight,
child: Container(
margin: const EdgeInsets.all(12),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(99),
),
child: const Text(
'RARE',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
letterSpacing: 1,
),
),
),
);
}
}
+1
View File
@@ -0,0 +1 @@
flutter/ephemeral
+128
View File
@@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "holo_shiny_example")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.holo_shiny_example")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
+88
View File
@@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)
@@ -0,0 +1,11 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void fl_register_plugins(FlPluginRegistry* registry) {
}
@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_
@@ -0,0 +1,23 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)
+26
View File
@@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+6
View File
@@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}
+148
View File
@@ -0,0 +1,148 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView* view) {
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "holo_shiny_example");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "holo_shiny_example");
}
gtk_window_set_default_size(window, 1280, 720);
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(
project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
GdkRGBA background_color;
// Background defaults to black, override it here if necessary, e.g. #00000000
// for transparent.
gdk_rgba_parse(&background_color, "#000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
// Show the window when Flutter renders.
// Requires the view to be realized so we can start rendering.
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
self);
gtk_widget_realize(GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application,
gchar*** arguments,
int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line =
my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID, "flags",
G_APPLICATION_NON_UNIQUE, nullptr));
}
+21
View File
@@ -0,0 +1,21 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication,
my_application,
MY,
APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_
+23
View File
@@ -0,0 +1,23 @@
name: holo_shiny_example
description: Example app for holo_shiny.
publish_to: none
environment:
sdk: ^3.6.0
flutter: ">=3.22.0"
dependencies:
flutter:
sdk: flutter
holo_shiny:
path: ..
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
uses-material-design: true
assets:
- assets/
+3
View File
@@ -0,0 +1,3 @@
export 'src/sensor_tilt_controller.dart';
export 'src/shiny_controller.dart';
export 'src/shiny_widget.dart';
+20
View File
@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Text('Hello World!'),
),
),
);
}
}
+122
View File
@@ -0,0 +1,122 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:sensors_plus/sensors_plus.dart';
/// Produces a normalized tilt stream from gyroscope and accelerometer data.
class SensorTiltController {
/// Creates and starts the sensor controller.
SensorTiltController({
this.alpha = 0.98,
this.maxTiltRadians = 0.5,
}) : _invAlpha = 1.0 - alpha {
_start();
}
/// Complementary filter blending coefficient.
final double alpha;
/// Cached inverse alpha to avoid repetitive subtractions.
final double _invAlpha;
/// Physical angle that maps to normalized magnitude `1.0`.
final double maxTiltRadians;
final StreamController<Offset> _controller =
StreamController<Offset>.broadcast();
// High-performance timing for tight physics loops
final Stopwatch _stopwatch = Stopwatch();
StreamSubscription<GyroscopeEvent>? _gyroSub;
StreamSubscription<AccelerometerEvent>? _accelSub;
double _roll = 0.0;
double _pitch = 0.0;
double _accelRoll = 0.0;
double _accelPitch = 0.0;
int? _lastMicroseconds;
/// The normalized tilt stream.
Stream<Offset> get stream => _controller.stream;
void _start() {
if (!_supportsSensorStreams) return;
_stopwatch.start();
try {
_gyroSub = gyroscopeEventStream().listen(
_onGyro,
onError: (_) {},
cancelOnError: false,
);
} on MissingPluginException {
_gyroSub = null;
}
try {
_accelSub = accelerometerEventStream().listen(
_onAccel,
onError: (_) {},
cancelOnError: false,
);
} on MissingPluginException {
_accelSub = null;
}
}
bool get _supportsSensorStreams {
if (kIsWeb) return true;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
return true;
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
return false;
}
}
void _onGyro(GyroscopeEvent event) {
final int nowMicros = _stopwatch.elapsedMicroseconds;
final int? last = _lastMicroseconds;
_lastMicroseconds = nowMicros;
if (last == null) return;
final double dt = (nowMicros - last) / 1000000.0;
if (dt <= 0.0) return;
final double gyroRoll = _roll + (event.y * dt);
final double gyroPitch = _pitch + (event.x * dt);
_roll = (alpha * gyroRoll) + (_invAlpha * _accelRoll);
_pitch = (alpha * gyroPitch) + (_invAlpha * _accelPitch);
_controller.add(_normalizedOffset());
}
void _onAccel(AccelerometerEvent event) {
const double g = 9.81;
_accelRoll = (event.x / g).clamp(-1.0, 1.0);
_accelPitch = (event.y / g).clamp(-1.0, 1.0);
}
Offset _normalizedOffset() {
final double dx = (_roll / maxTiltRadians).clamp(-1.0, 1.0);
final double dy = (_pitch / maxTiltRadians).clamp(-1.0, 1.0);
return Offset(dx, dy);
}
/// Stops sensor streams and releases resources.
Future<void> dispose() async {
_stopwatch.stop();
await _gyroSub?.cancel();
await _accelSub?.cancel();
await _controller.close();
}
}
+47
View File
@@ -0,0 +1,47 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'sensor_tilt_controller.dart';
/// Optional controller for providing tilt input to [Shiny] widgets.
///
/// By default, no stream is emitted. Set [useSensor] to true to use built-in
/// motion sensors, or provide [tiltStream] to fully control tilt externally.
class ShinyController {
/// Creates a [ShinyController].
///
/// If [tiltStream] is provided, it overrides the built-in sensor stream.
ShinyController({
this.useSensor = false,
this.tiltStream,
}) {
if (tiltStream == null && useSensor) {
_sensorController = SensorTiltController();
}
}
/// Enables internal sensor-based tilt when [tiltStream] is null.
final bool useSensor;
/// Optional external tilt stream in normalized `[-1.0, 1.0]` space.
final Stream<Offset>? tiltStream;
SensorTiltController? _sensorController;
/// The effective tilt stream consumed by [Shiny].
Stream<Offset> get stream {
if (tiltStream != null) {
return tiltStream!;
}
if (_sensorController != null) {
return _sensorController!.stream;
}
return const Stream<Offset>.empty();
}
/// Disposes internally-owned resources.
Future<void> dispose() async {
await _sensorController?.dispose();
}
}
+490
View File
@@ -0,0 +1,490 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'shiny_controller.dart';
enum HolographStyle {
holographicSilver,
crackedIce,
silverMosaic,
superGoldVinyl,
}
enum SparkleShapeKind {
star,
rectangle,
polygon,
randomPolygon,
confetti,
}
class SparkleShapeSpec {
const SparkleShapeSpec._({
required this.kind,
required this.primary,
required this.secondary,
required this.tertiary,
});
static const SparkleShapeSpec eightPointStar = SparkleShapeSpec._(
kind: SparkleShapeKind.star,
primary: 8.0,
secondary: 0.42,
tertiary: 0.0,
);
static const SparkleShapeSpec fivePointStar = SparkleShapeSpec._(
kind: SparkleShapeKind.star,
primary: 5.0,
secondary: 0.42,
tertiary: 0.0,
);
static const SparkleShapeSpec rectangle = SparkleShapeSpec._(
kind: SparkleShapeKind.rectangle,
primary: 0.24,
secondary: 0.055,
tertiary: 1.0,
);
static const SparkleShapeSpec diamond = SparkleShapeSpec._(
kind: SparkleShapeKind.polygon,
primary: 4.0,
secondary: 1.0,
tertiary: 0.78539816339,
);
static const SparkleShapeSpec hexagon = SparkleShapeSpec._(
kind: SparkleShapeKind.polygon,
primary: 6.0,
secondary: 1.0,
tertiary: 0.0,
);
static const SparkleShapeSpec randomPolygon = SparkleShapeSpec._(
kind: SparkleShapeKind.randomPolygon,
primary: 0.0,
secondary: 0.0,
tertiary: 0.0,
);
static const SparkleShapeSpec confetti = SparkleShapeSpec._(
kind: SparkleShapeKind.confetti,
primary: 0.34,
secondary: 0.045,
tertiary: 1.0,
);
factory SparkleShapeSpec.customStar(
{int points = 8, double innerRatio = 0.42}) {
return SparkleShapeSpec._(
kind: SparkleShapeKind.star,
primary: points.toDouble().clamp(4.0, 12.0),
secondary: innerRatio.clamp(0.15, 0.85),
tertiary: 0.0,
);
}
factory SparkleShapeSpec.customPolygon(
{int sides = 6, double aspectRatio = 1.0, double rotation = 0.0}) {
return SparkleShapeSpec._(
kind: SparkleShapeKind.polygon,
primary: sides.toDouble().clamp(3.0, 10.0),
secondary: aspectRatio.clamp(0.4, 2.0),
tertiary: rotation,
);
}
factory SparkleShapeSpec.customRectangle(
{double halfWidth = 0.24,
double halfHeight = 0.055,
double rotationJitter = 1.0}) {
return SparkleShapeSpec._(
kind: SparkleShapeKind.rectangle,
primary: halfWidth.clamp(0.05, 0.45),
secondary: halfHeight.clamp(0.02, 0.30),
tertiary: rotationJitter.clamp(0.0, 1.0),
);
}
factory SparkleShapeSpec.customConfetti(
{double length = 0.34,
double thickness = 0.045,
double rotationJitter = 1.0}) {
return SparkleShapeSpec._(
kind: SparkleShapeKind.confetti,
primary: length.clamp(0.08, 0.48),
secondary: thickness.clamp(0.01, 0.14),
tertiary: rotationJitter.clamp(0.0, 1.0),
);
}
final SparkleShapeKind kind;
final double primary;
final double secondary;
final double tertiary;
}
class Shiny extends StatefulWidget {
const Shiny({
super.key,
required this.child,
this.controller,
this.tilt,
this.prismatic = 0.8,
this.sparkle = 0.8,
this.specular = 0.8,
this.diffraction = 0.8,
this.sparkleShape = SparkleShapeSpec.eightPointStar,
this.style = HolographStyle.crackedIce,
this.blendMode = BlendMode.screen,
this.enableShader = true,
});
final Widget child;
final ShinyController? controller;
final Offset? tilt;
final double prismatic;
final double sparkle;
final double specular;
final double diffraction;
final SparkleShapeSpec sparkleShape;
final HolographStyle style;
final BlendMode blendMode;
final bool enableShader;
@override
State<Shiny> createState() => _ShinyState();
}
class _ShinyState extends State<Shiny> with TickerProviderStateMixin {
static Future<ui.FragmentProgram>? _programFuture;
ui.FragmentShader? _shader;
StreamSubscription<Offset>? _tiltSub;
late final Ticker _ticker;
double _time = 0.0;
Offset _tilt = Offset.zero;
@override
void initState() {
super.initState();
_ticker = createTicker((Duration elapsed) {
if (!mounted || _shader == null) return;
setState(() {
_time = elapsed.inMicroseconds / 1000000.0;
});
});
_attachController();
if (widget.enableShader) {
_loadShader();
_ticker.start();
}
}
@override
void didUpdateWidget(covariant Shiny oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.controller != widget.controller ||
oldWidget.tilt != widget.tilt) {
_tiltSub?.cancel();
_attachController();
}
if (oldWidget.enableShader != widget.enableShader) {
if (widget.enableShader) {
if (_shader == null) _loadShader();
_ticker.start();
} else {
_ticker.stop();
}
}
}
Future<void> _loadShader() async {
try {
_programFuture ??= _loadProgram();
final ui.FragmentProgram program = await _programFuture!;
if (!mounted) return;
setState(() {
_shader = program.fragmentShader();
});
} catch (_) {
// Keep rendering without shader when runtime effects are unavailable.
}
}
Future<ui.FragmentProgram> _loadProgram() async {
try {
return await ui.FragmentProgram.fromAsset('shaders/shiny_card.frag');
} catch (_) {
return await ui.FragmentProgram.fromAsset(
'packages/holo_shiny/shaders/shiny_card.frag');
}
}
void _attachController() {
if (widget.tilt != null) return;
final ShinyController? controller = widget.controller;
if (controller == null) return;
_tiltSub = controller.stream.listen((Offset value) {
if (!mounted || widget.tilt != null) return;
final clamped = _clampOffset(value);
if (_tilt != clamped) {
setState(() {
_tilt = clamped;
});
}
});
}
@override
void dispose() {
_ticker.dispose();
_tiltSub?.cancel();
_shader?.dispose();
super.dispose();
}
ui.Shader _configureShader(Rect bounds) {
final Offset effectiveTilt = _clampOffset(widget.tilt ?? _tilt);
final ui.FragmentShader shader = _shader!;
shader
..setFloat(0, bounds.width)
..setFloat(1, bounds.height)
..setFloat(2, effectiveTilt.dx)
..setFloat(3, effectiveTilt.dy)
..setFloat(4, _time)
..setFloat(5, widget.prismatic)
..setFloat(6, widget.sparkle)
..setFloat(7, widget.specular)
..setFloat(8, widget.diffraction)
..setFloat(9, widget.style.index.toDouble())
..setFloat(10, widget.sparkleShape.kind.index.toDouble())
..setFloat(11, widget.sparkleShape.primary)
..setFloat(12, widget.sparkleShape.secondary)
..setFloat(13, widget.sparkleShape.tertiary);
return shader;
}
@override
Widget build(BuildContext context) {
if (!widget.enableShader || _shader == null) return widget.child;
return ShaderMask(
blendMode: widget.blendMode,
shaderCallback: _configureShader,
child: widget.child,
);
}
Offset _clampOffset(Offset value) {
return Offset(value.dx.clamp(-1.0, 1.0), value.dy.clamp(-1.0, 1.0));
}
}
class ShinyCard extends StatefulWidget {
const ShinyCard({
super.key,
this.background,
this.foreground,
this.shape = const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16))),
this.width = 300,
this.height = 420,
this.controller,
this.prismatic = 0.8,
this.sparkle = 0.8,
this.specular = 0.8,
this.diffraction = 0.8,
this.sparkleShape = SparkleShapeSpec.eightPointStar,
this.style = HolographStyle.crackedIce,
this.enableShader = true,
});
final Widget? background;
final Widget? foreground;
final ShapeBorder shape;
final double width;
final double height;
final ShinyController? controller;
final double prismatic;
final double sparkle;
final double specular;
final double diffraction;
final SparkleShapeSpec sparkleShape;
final HolographStyle style;
final bool enableShader;
@override
State<ShinyCard> createState() => _ShinyCardState();
}
class _ShinyCardState extends State<ShinyCard>
with SingleTickerProviderStateMixin {
static const ValueKey<String> transformKey =
ValueKey<String>('holo_shiny.card.transform');
StreamSubscription<Offset>? _tiltSub;
late final AnimationController _spring;
// State segregation: prevents total widget rebuilds during drag events
late final ValueNotifier<Offset> _tiltNotifier = ValueNotifier(Offset.zero);
late final StreamController<Offset> _internalTiltStream =
StreamController<Offset>.broadcast();
late final ShinyController _internalShinyController =
ShinyController(tiltStream: _internalTiltStream.stream);
Offset _sensorTilt = Offset.zero;
Offset _dragTilt = Offset.zero;
Offset _dragTiltAtRelease = Offset.zero;
bool _isDragging = false;
bool _isReturning = false;
void _updateTilt() {
final tilt = (_isDragging || _isReturning) ? _dragTilt : _sensorTilt;
if (_tiltNotifier.value != tilt) {
_tiltNotifier.value = tilt;
if (!_internalTiltStream.isClosed) {
_internalTiltStream.add(tilt);
}
}
}
@override
void initState() {
super.initState();
_spring = AnimationController(
vsync: this, duration: const Duration(milliseconds: 850))
..addListener(() {
if (!mounted || !_isReturning) return;
final double t = Curves.elasticOut.transform(_spring.value);
_dragTilt =
Offset.lerp(_dragTiltAtRelease, Offset.zero, t) ?? Offset.zero;
_updateTilt();
})
..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
_isReturning = false;
_updateTilt();
}
});
_attachController();
}
@override
void didUpdateWidget(covariant ShinyCard oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.controller != widget.controller) {
_tiltSub?.cancel();
_attachController();
}
}
void _attachController() {
final ShinyController? controller = widget.controller;
if (controller == null) return;
_tiltSub = controller.stream.listen((Offset value) {
if (!mounted || _isDragging || _isReturning) return;
_sensorTilt = _clampOffset(value);
_updateTilt();
});
}
void _onPanUpdate(DragUpdateDetails details) {
_isDragging = true;
_isReturning = false;
_spring.stop();
final double dx = (details.delta.dx / widget.width) * 2.0;
final double dy = (details.delta.dy / widget.height) * 2.0;
_dragTilt = _clampOffset(_dragTilt + Offset(dx, dy));
_updateTilt();
}
void _onPanEnd(DragEndDetails details) {
_isDragging = false;
_isReturning = true;
_dragTiltAtRelease = _dragTilt;
_spring
..reset()
..forward();
}
@override
void dispose() {
_tiltSub?.cancel();
_spring.dispose();
_tiltNotifier.dispose();
_internalTiltStream.close();
_internalShinyController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final Widget base = widget.background ??
const ColoredBox(color: Color(0xFF212121), child: SizedBox.expand());
// Isolate the static tree structure so it never rebuilds when tilted
final Widget staticChild = ClipPath(
clipper: ShapeBorderClipper(shape: widget.shape),
child: SizedBox(
width: widget.width,
height: widget.height,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
Shiny(
enableShader: widget.enableShader,
controller: _internalShinyController,
tilt: null, // Managed natively via the internal stream controller
prismatic: widget.prismatic,
sparkle: widget.sparkle,
specular: widget.specular,
diffraction: widget.diffraction,
sparkleShape: widget.sparkleShape,
style: widget.style,
child: base,
),
if (widget.foreground != null) widget.foreground!,
],
),
),
);
return GestureDetector(
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
child: ValueListenableBuilder<Offset>(
valueListenable: _tiltNotifier,
builder: (context, tilt, child) {
final Matrix4 transform = Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateX(-tilt.dy * 0.3)
..rotateY(tilt.dx * 0.3);
return Transform(
key: transformKey,
alignment: Alignment.center,
transform: transform,
child: child,
);
},
child: staticChild,
),
);
}
Offset _clampOffset(Offset value) {
return Offset(value.dx.clamp(-1.0, 1.0), value.dy.clamp(-1.0, 1.0));
}
}
+26
View File
@@ -0,0 +1,26 @@
name: holo_shiny
description: Holographic shiny card widget powered by GLSL fragment shaders.
version: 0.1.0
publish_to: none
repository: https://example.com/your-org/holo_shiny
homepage: https://example.com/your-org/holo_shiny
issue_tracker: https://example.com/your-org/holo_shiny/issues
documentation: https://example.com/your-org/holo_shiny/docs
environment:
sdk: ^3.6.0
flutter: ">=3.22.0"
dependencies:
flutter:
sdk: flutter
sensors_plus: ^7.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
shaders:
- shaders/shiny_card.frag
+378
View File
@@ -0,0 +1,378 @@
#version 460 core
#include <flutter/runtime_effect.glsl>
out vec4 fragColor;
uniform vec2 uSize;
uniform vec2 uTilt;
uniform float uTime;
uniform float uPrismatic;
uniform float uSparkle;
uniform float uSpecular;
uniform float uDiffraction;
uniform float uStyle;
uniform float uSparkleShapeKind;
uniform float uSparklePrimary;
uniform float uSparkleSecondary;
uniform float uSparkleTertiary;
#define TWO_PI 6.28318530718
// Precomputed rotation matrices to avoid expensive runtime sin/cos calculations
#define ROT_0_78 mat2( 0.71091, 0.70328, -0.70328, 0.71091)
#define ROT_M0_5 mat2( 0.87758, -0.47943, 0.47943, 0.87758)
#define ROT_1_2 mat2( 0.36236, 0.93204, -0.93204, 0.36236)
float hash(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
vec3 hsv2rgb(float h, float s, float v) {
vec3 k = vec3(1.0, 2.0 / 3.0, 1.0 / 3.0);
vec3 p = abs(fract(vec3(h) + k) * 6.0 - 3.0);
return v * mix(vec3(1.0), clamp(p - 1.0, 0.0, 1.0), s);
}
vec3 rainbow(float phase, float saturation, float value) {
return hsv2rgb(fract(phase), saturation, value);
}
float sdBox(vec2 p, vec2 b) {
vec2 d = abs(p) - b;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
float sdRegularPolygon(vec2 p, float sides, float radius) {
float angle = atan(p.y, p.x);
float sector = TWO_PI / sides;
return cos(floor(0.5 + angle / sector) * sector - angle) * length(p) - radius;
}
float sparkleStarMask(vec2 p, float points, float innerRatio) {
float angle = atan(p.y, p.x);
float radius = dot(p, p); // Optimized: Use squared distance to delay sqrt
float spikes = 0.5 + 0.5 * cos(angle * points);
float starRadius = mix(0.34 * innerRatio, 0.34, pow(spikes, 1.4));
return smoothstep(0.0009, -0.0009, radius - (starRadius * starRadius));
}
vec2 rotatePoint(vec2 p, float rotation) {
float c = cos(rotation);
float s = sin(rotation);
return vec2(c * p.x - s * p.y, s * p.x + c * p.y);
}
float sparkleRectangleMask(vec2 p, float halfWidth, float halfHeight, float rotation) {
vec2 q = rotatePoint(p, rotation);
float body = sdBox(q, vec2(halfWidth, halfHeight));
float bevel = sdBox(q, vec2(halfWidth * 0.78, halfHeight * 0.78));
float bodyMask = smoothstep(0.03, -0.03, body);
float centerLine = 1.0 - smoothstep(0.0, halfHeight * 1.25, abs(q.y));
float endFade = 1.0 - smoothstep(halfWidth * 0.55, halfWidth * 1.05, abs(q.x));
float streak = centerLine * endFade;
float bevelRing = smoothstep(0.025, -0.025, body) - smoothstep(0.025, -0.025, bevel);
return clamp(bodyMask * (0.58 + 0.42 * streak) + bevelRing * 0.25, 0.0, 1.0);
}
float sparklePolygonMask(vec2 p, float sides, float aspectRatio, float rotation) {
vec2 q = rotatePoint(p, rotation);
q.x /= aspectRatio;
float poly = sdRegularPolygon(q, sides, 0.26);
return smoothstep(0.03, -0.03, poly);
}
float sparkleShapeMask(vec2 p, vec2 id) {
float randomRotation = hash(id + 5.1) * TWO_PI;
if (uSparkleShapeKind < 0.5) {
return sparkleStarMask(p, uSparklePrimary, uSparkleSecondary);
}
if (uSparkleShapeKind < 1.5) {
return sparkleRectangleMask(p, uSparklePrimary, uSparkleSecondary, randomRotation * uSparkleTertiary);
}
if (uSparkleShapeKind < 2.5) {
return sparklePolygonMask(p, uSparklePrimary, uSparkleSecondary, uSparkleTertiary);
}
if (uSparkleShapeKind < 3.5) {
float sides = 5.0 + floor(hash(id + 9.4) * 4.0);
float aspect = 0.7 + hash(id + 13.1) * 0.7;
return sparklePolygonMask(p, sides, aspect, randomRotation);
}
return sparkleRectangleMask(p, uSparklePrimary, uSparkleSecondary, randomRotation * uSparkleTertiary);
}
vec3 styleHolographicSilver(vec2 uv, vec2 tilt, float time) {
vec2 p = uv * 2.0 - 1.0;
float flowA = noise(uv * 3.5 + vec2(time * 0.03, -time * 0.02));
float flowB = noise(uv * 7.0 + vec2(-time * 0.02, time * 0.03));
vec2 warp = p + 0.25 * vec2(flowA - 0.5, flowB - 0.5);
float ribbon = sin(dot(warp, vec2(6.5, 2.8)) + flowA * 5.0 + dot(tilt, vec2(2.2, -1.7)));
float plume = sin(length(warp + vec2(flowB - 0.5, flowA - 0.5)) * 9.0 - tilt.x * 3.0 + time * 0.2);
float phase = flowA * 1.8 + ribbon * 0.28 + plume * 0.22 + time * 0.03;
vec3 holo = rainbow(phase, 0.72, 1.0);
float blend = smoothstep(-0.65, 0.95, ribbon + plume * 0.55);
vec3 silver = vec3(0.62, 0.65, 0.70) * (0.85 + 0.15 * flowB);
return mix(silver, holo, 0.65 * blend);
}
vec3 styleCrackedIce(vec2 uv, vec2 tilt, float time) {
float maxZ = -999.0;
vec2 bestSlope = vec2(0.0);
float bestHash = 0.0;
float bestEdge = 1.0;
vec2 warp = vec2(sin(uv.y * 10.0), cos(uv.x * 10.0)) * 0.05;
vec2 baseUV = uv + warp;
// --- LAYER 1 ---
vec2 p1 = baseUV * 12.0;
vec2 id1 = floor(p1);
vec2 f1 = fract(p1) - 0.5;
vec2 slope1 = vec2(hash(id1 + 1.2), hash(id1 + 1.3)) * 2.0 - 1.0;
float z1 = dot(f1, slope1) * 3.5;
if (z1 > maxZ) {
maxZ = z1; bestSlope = slope1; bestHash = hash(id1 + 1.1); bestEdge = min(0.5 - abs(f1.x), 0.5 - abs(f1.y));
}
// --- LAYER 2 (Optimized Rotation) ---
vec2 p2 = (ROT_0_78 * baseUV) * 15.0;
vec2 id2 = floor(p2);
vec2 f2 = fract(p2) - 0.5;
vec2 slope2 = vec2(hash(id2 + 2.2), hash(id2 + 2.3)) * 2.0 - 1.0;
float z2 = 0.8 + dot(f2, slope2) * 3.5;
if (z2 > maxZ) {
maxZ = z2; bestSlope = slope2; bestHash = hash(id2 + 2.1); bestEdge = min(0.5 - abs(f2.x), 0.5 - abs(f2.y));
}
// --- LAYER 3 (Optimized Rotation) ---
vec2 p3 = (ROT_M0_5 * baseUV) * 18.0;
vec2 id3 = floor(p3);
vec2 f3 = fract(p3) - 0.5;
vec2 slope3 = vec2(hash(id3 + 3.2), hash(id3 + 3.3)) * 2.0 - 1.0;
float z3 = 1.6 + dot(f3, slope3) * 3.5;
if (z3 > maxZ) {
maxZ = z3; bestSlope = slope3; bestHash = hash(id3 + 3.1); bestEdge = min(0.5 - abs(f3.x), 0.5 - abs(f3.y));
}
// --- LAYER 4 (Optimized Rotation) ---
vec2 p4 = (ROT_1_2 * baseUV) * 21.0;
vec2 id4 = floor(p4);
vec2 f4 = fract(p4) - 0.5;
vec2 slope4 = vec2(hash(id4 + 4.2), hash(id4 + 4.3)) * 2.0 - 1.0;
float z4 = 2.4 + dot(f4, slope4) * 3.5;
if (z4 > maxZ) {
maxZ = z4; bestSlope = slope4; bestHash = hash(id4 + 4.1); bestEdge = min(0.5 - abs(f4.x), 0.5 - abs(f4.y));
}
// --- VIBRANT COLOR OPTICS ---
float alignment = dot(tilt, normalize(bestSlope + vec2(0.001)));
float phase = dot(uv, vec2(0.6, -0.4)) + time * 0.05 + bestHash * 0.6 + alignment * 0.5;
float saturation = 0.7 + 0.3 * hash(vec2(bestHash, 6.1));
vec3 baseColor = rainbow(phase, saturation, 1.0);
float lightCycle = sin(alignment * 4.0 + bestHash * TWO_PI + time * 0.15);
float ambient = 0.15 + 0.25 * max(0.0, lightCycle);
vec3 finalColor = baseColor * ambient;
float flash = pow(max(0.0, lightCycle), 5.0);
finalColor += mix(baseColor, vec3(1.0, 0.95, 0.85), 0.6) * flash * 0.55;
finalColor += baseColor * smoothstep(0.05, 0.0, bestEdge) * flash * 0.4;
return finalColor;
}
vec3 styleSilverMosaic(vec2 uv, vec2 tilt, float time) {
// Setup grid - Scaled up 300% (18.0 -> 6.0)
vec2 g = uv * 6.0;
vec2 id = floor(g);
// Center coordinates around 0.0 for the radial math
vec2 f = fract(g) - 0.5;
// Real foil sheets often alternate the grain of the squares to catch light from all angles.
if (mod(id.x + id.y, 2.0) > 0.5) {
f = vec2(-f.y, f.x);
}
// Get the radial vector (pointing outward from the center of the square)
vec2 radialDir = normalize(f + vec2(0.0001));
// Calculate the simulated light direction based on device tilt.
vec2 lightDir = normalize(tilt + vec2(0.001));
// --- THE OPTICS ---
// A radial highlight occurs where the radial vector aligns with the light vector.
float alignment = abs(dot(radialDir, lightDir));
// Raise to a high power to narrow the reflection into a sharp, focused beam
float highlight = pow(alignment, 12.0);
// --- THE DIFFRACTION (Rainbow) ---
float angleDiff = acos(alignment);
// 2D cross product to find which side of the highlight we are on
float side = sign(radialDir.x * lightDir.y - radialDir.y * lightDir.x);
// Phase shift based on tilt magnitude and time.
float basePhase = length(tilt) * 2.5 + time * 0.1;
// Calculate the final color phase.
float phase = basePhase + side * angleDiff * 1.8;
vec3 holoColor = rainbow(phase, 0.85, 1.0);
// Add a pure white "specular core" where alignment is absolutely perfect
vec3 core = vec3(1.0) * smoothstep(0.98, 1.0, alignment);
// Base foil material
vec3 foilBase = vec3(0.25, 0.27, 0.30);
// Combine the light components
vec3 finalColor = foilBase + (holoColor * highlight * 1.5) + (core * 0.8);
// --- BORDERS AND DEPTH ---
// Crisp borders between the squares
float edgeX = 0.5 - abs(f.x);
float edgeY = 0.5 - abs(f.y);
// Tightened the border width to compensate for the larger tile size
float border = smoothstep(0.0, 0.015, min(edgeX, edgeY));
// Add a subtle darkening toward the edges of the tiles
float dist = length(f);
finalColor *= mix(0.7, 1.0, 1.0 - smoothstep(0.0, 0.5, dist) * 0.3);
return clamp(finalColor * border, 0.0, 1.0);
}
vec3 styleSuperGoldVinyl(vec2 uv, vec2 tilt, float time) {
vec2 g = uv * 60.0;
vec2 staggered = vec2(g.x + 0.5 * mod(floor(g.y), 2.0), g.y);
vec2 f = fract(staggered) - 0.5;
// Optimized: Use dot product for squared distance to avoid expensive sqrt/length.
// Original smoothstep values (0.42, 0.10) squared become (0.1764, 0.0100)
// Original smoothstep values (0.45, 0.18) squared become (0.2025, 0.0324)
float d2 = dot(f, f);
float dotMask = smoothstep(0.1764, 0.01, d2);
float emboss = smoothstep(0.2025, 0.0324, d2);
float sheen = 0.5 + 0.5 * sin(dot(uv, vec2(18.0, -6.0)) + noise(uv * 6.0) * 2.5 + dot(tilt, vec2(2.4, 1.8)) * 2.0 + time * 0.2);
vec3 gold = vec3(0.84, 0.70, 0.24);
vec3 warm = vec3(1.0, 0.88, 0.42);
vec3 shadow = vec3(0.18, 0.14, 0.05);
return gold * (0.75 + 0.25 * sheen) + warm * dotMask * 0.28 + shadow * emboss * 0.08;
}
void main() {
// Standard normalized UVs [0.0 to 1.0] for macro lighting, tilt, and glare
vec2 uv = FlutterFragCoord().xy / uSize;
vec2 center = vec2(0.5, 0.5);
vec2 fromCenter = uv - center;
// Aspect-corrected UVs for physical patterns (grid, sparkles, mosaics)
// Dividing by the maximum dimension ensures squares remain squares.
float maxDim = max(uSize.x, uSize.y);
vec2 aspectUV = FlutterFragCoord().xy / maxDim;
float tiltMag = length(uTilt);
float safeTiltMag = max(tiltMag, 0.001);
vec2 tiltDir = uTilt / safeTiltMag;
// The highlight uses the stretched UV so the glare covers the whole widget nicely
float highlightDistance = length(uv - (center + (uTilt * 0.35)));
float specularMask = pow(max(0.0, 1.0 - highlightDistance * 2.6), 3.2);
vec3 specular = vec3(specularMask) * uSpecular * (0.4 + 0.6 * tiltMag);
bool isCrackedIce = uStyle >= 0.5 && uStyle < 1.5;
float sparkleGridScale = isCrackedIce ? 7.0 : 18.0;
// ---> USE aspectUV HERE so sparkles are drawn perfectly proportionally
vec2 sparkleGrid = aspectUV * sparkleGridScale;
vec2 grid = floor(sparkleGrid);
float rarityThreshold = isCrackedIce ? 0.965 : 0.83;
float rarity = step(rarityThreshold, hash(grid + 0.31));
vec3 sparkle = vec3(0.0);
if (rarity > 0.0) {
vec2 cell = fract(sparkleGrid) - 0.5;
float sparkleHash = hash(grid);
vec2 sparkleNormal = normalize(vec2(sparkleHash * 2.0 - 1.0, hash(grid + 19.7) * 2.0 - 1.0));
float alignment = max(0.0, dot(tiltDir, sparkleNormal));
if (alignment > 0.0) {
float twinkle = 0.5 + 0.5 * sin(uTime * 5.5 + sparkleHash * TWO_PI);
float shapeMask = 0.0;
if (isCrackedIce) {
float sides = 5.0 + floor(hash(grid + 9.4) * 4.0);
float aspect = 0.75 + hash(grid + 13.1) * 0.55;
shapeMask = sparklePolygonMask(cell, sides, aspect, hash(grid + 5.1) * TWO_PI);
} else {
shapeMask = sparkleShapeMask(cell, grid);
}
float sparklePower = isCrackedIce ? 4.8 : 3.2;
float sparkleIntensity = isCrackedIce ? 0.25 : 1.0;
float sparkleChroma = isCrackedIce ? 0.20 : 0.35;
float sparkleMask = pow(alignment, sparklePower) * twinkle * rarity * shapeMask;
vec3 sparkleColor = mix(vec3(1.0), rainbow(sparkleHash + dot(uTilt, vec2(0.2, -0.15)), 0.55, 1.0), sparkleChroma);
sparkle = sparkleColor * sparkleMask * uSparkle * sparkleIntensity;
}
}
vec3 styleBase;
// ---> USE aspectUV HERE so the foil patterns tile properly
if (uStyle < 0.5) {
styleBase = styleHolographicSilver(aspectUV, uTilt, uTime);
} else if (uStyle < 1.5) {
styleBase = styleCrackedIce(aspectUV, uTilt, uTime);
} else if (uStyle < 2.5) {
styleBase = styleSilverMosaic(aspectUV, uTilt, uTime);
} else {
styleBase = styleSuperGoldVinyl(aspectUV, uTilt, uTime);
}
float styleLuma = dot(styleBase, vec3(0.299, 0.587, 0.114));
vec3 chromaAdjusted = mix(vec3(styleLuma), styleBase, 0.2 + 0.8 * uPrismatic);
// General 3D lighting normal uses the stretched UV
vec2 normal2d = normalize(fromCenter + vec2(0.001));
float directional = 0.5 + 0.5 * dot(normal2d, tiltDir);
float tiltLighting = mix(0.86, 1.20, pow(directional, 1.2));
chromaAdjusted *= tiltLighting;
float microStrength = isCrackedIce ? 0.08 : 0.18;
// ---> USE aspectUV HERE so the microscopic diffraction doesn't stretch
float microMask = 0.5 + 0.5 * sin(dot(aspectUV, vec2(137.0, 57.0)) + noise(aspectUV * 14.0) * 4.0 + dot(uTilt, vec2(9.0, 4.0)) * 2.0);
vec3 microShimmer = rainbow(noise(aspectUV * 10.0) + dot(uTilt, vec2(0.4, -0.3)) * 0.3, 0.65, 1.0) * microMask * uDiffraction * microStrength;
vec3 styleColor = chromaAdjusted + microShimmer + (specular * 0.15) + sparkle;
vec3 mappedColor = styleColor / (1.0 + styleColor * 0.3);
float luma = dot(mappedColor, vec3(0.299, 0.587, 0.114));
vec3 vibrantColor = mix(vec3(luma), mappedColor, 1.4);
float foilBrightness = dot(vibrantColor, vec3(0.333));
float alpha = clamp(foilBrightness * 0.5, 0.0, 0.35);
vec3 finalColor = clamp(vibrantColor, 0.0, 1.0);
fragColor = vec4(finalColor * alpha, alpha);
}
+28
View File
@@ -0,0 +1,28 @@
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:holo_shiny/holo_shiny.dart';
void main() {
test('ShinyController uses external stream when provided', () async {
final StreamController<Offset> source =
StreamController<Offset>.broadcast();
final ShinyController controller =
ShinyController(useSensor: true, tiltStream: source.stream);
final Future<Offset> next = controller.stream.first;
source.add(const Offset(0.5, -0.25));
expect(await next, const Offset(0.5, -0.25));
await controller.dispose();
await source.close();
});
test('ShinyController emits nothing by default', () async {
final ShinyController controller = ShinyController();
final bool hasEvent = await controller.stream.isEmpty;
expect(hasEvent, isTrue);
await controller.dispose();
});
}
+153
View File
@@ -0,0 +1,153 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:holo_shiny/holo_shiny.dart';
const ValueKey<String> _cardTransformKey =
ValueKey<String>('holo_shiny.card.transform');
void main() {
test('default holograph style is crackedIce', () {
const Shiny shiny = Shiny(child: SizedBox.shrink());
const ShinyCard shinyCard = ShinyCard();
expect(shiny.style, HolographStyle.crackedIce);
expect(shinyCard.style, HolographStyle.crackedIce);
});
test('default sparkle shape is star', () {
const Shiny shiny = Shiny(child: SizedBox.shrink());
const ShinyCard shinyCard = ShinyCard();
expect(shiny.sparkleShape.kind, SparkleShapeKind.star);
expect(shiny.sparkleShape.primary, 8.0);
expect(shinyCard.sparkleShape.kind, SparkleShapeKind.star);
expect(shinyCard.sparkleShape.primary, 8.0);
});
test('custom sparkle factories produce expected specs', () {
final SparkleShapeSpec star =
SparkleShapeSpec.customStar(points: 7, innerRatio: 0.33);
final SparkleShapeSpec polygon = SparkleShapeSpec.customPolygon(
sides: 8, aspectRatio: 1.2, rotation: 0.4);
final SparkleShapeSpec rectangle = SparkleShapeSpec.customRectangle(
halfWidth: 0.20, halfHeight: 0.05, rotationJitter: 0.7);
expect(star.kind, SparkleShapeKind.star);
expect(star.primary, 7.0);
expect(star.secondary, 0.33);
expect(polygon.kind, SparkleShapeKind.polygon);
expect(polygon.primary, 8.0);
expect(polygon.secondary, 1.2);
expect(polygon.tertiary, 0.4);
expect(rectangle.kind, SparkleShapeKind.rectangle);
expect(rectangle.primary, 0.20);
expect(rectangle.secondary, 0.05);
expect(rectangle.tertiary, 0.7);
});
testWidgets('Shiny wraps any child without card transform',
(WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Shiny(
enableShader: false,
child: ColoredBox(
color: Colors.blue,
child: SizedBox(width: 80, height: 40),
),
),
),
),
);
expect(find.byType(ColoredBox), findsWidgets);
expect(find.byKey(_cardTransformKey), findsNothing);
});
testWidgets('ShinyCard renders clipping and foreground',
(WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: ShinyCard(
enableShader: false,
background: ColoredBox(color: Colors.black),
foreground: Text('FOREGROUND'),
),
),
),
);
expect(find.text('FOREGROUND'), findsOneWidget);
expect(find.byType(ClipPath), findsOneWidget);
expect(find.byKey(_cardTransformKey), findsOneWidget);
});
testWidgets('ShinyCard responds to external tilt stream',
(WidgetTester tester) async {
final StreamController<Offset> stream =
StreamController<Offset>.broadcast();
final ShinyController controller =
ShinyController(tiltStream: stream.stream);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: ShinyCard(
enableShader: false,
controller: controller,
background: const ColoredBox(color: Colors.black),
),
),
),
),
);
final Matrix4 before =
tester.widget<Transform>(find.byKey(_cardTransformKey)).transform;
stream.add(const Offset(0.7, 0.2));
await tester.pump();
final Matrix4 after =
tester.widget<Transform>(find.byKey(_cardTransformKey)).transform;
expect(after.storage[8], isNot(before.storage[8]));
await controller.dispose();
await stream.close();
});
testWidgets('Dragging ShinyCard updates transform',
(WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Center(
child: ShinyCard(
enableShader: false,
background: ColoredBox(color: Colors.black),
),
),
),
),
);
final Finder shinyCard = find.byType(ShinyCard);
final Matrix4 before =
tester.widget<Transform>(find.byKey(_cardTransformKey)).transform;
await tester.drag(shinyCard, const Offset(50, 20));
await tester.pump();
final Matrix4 after =
tester.widget<Transform>(find.byKey(_cardTransformKey)).transform;
expect(after.storage[8], isNot(before.storage[8]));
});
}